From c27662540f4ae637c66f394c552cc7721a04cf23 Mon Sep 17 00:00:00 2001 From: Marcin Procyk Date: Fri, 17 Dec 2021 19:29:26 +0100 Subject: [PATCH] feat(listsADM): add canDeleteList route (#1968) * add route and messages * add sparql query & responder methods * minor fixes * remove unused prefixes * add tests * review changes Co-authored-by: Ivan Subotic <400790+subotic@users.noreply.github.com> --- .../listsmessages/ListsMessagesADM.scala | 22 ++++++ .../responders/admin/ListsResponderADM.scala | 27 +++++++ .../admin/lists/DeleteListItemsRouteADM.scala | 32 +++++++- .../sparql/admin/canDeleteList.scala.txt | 29 ++++++++ .../DeleteListItemsRouteADME2ESpec.scala | 46 ++++++++++++ .../admin/ListsResponderADMSpec.scala | 74 +++++++++++++++++++ 6 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/canDeleteList.scala.txt 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 8f53309547..1230afae7e 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 @@ -332,6 +332,16 @@ case class ListItemDeleteRequestADM( apiRequestID: UUID ) extends ListsResponderRequestADM +/** + * Request checks if a list is unused and can be deleted. A successful response will be a [[CanDeleteListResponseADM]] + * + * @param iri the IRI of the list node (root or child). + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the user making the request. + */ +case class CanDeleteListRequestADM(iri: IRI, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM) + extends ListsResponderRequestADM + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Responses @@ -426,6 +436,16 @@ case class ChildNodeDeleteResponseADM(node: ListNodeADM) extends ListItemDeleteR def toJsValue: JsValue = listNodeDeleteResponseADMFormat.write(this) } +/** + * Checks if a list can be deleted (none of its nodes is used in data). + * + * @param iri the IRI of the list that is checked. + */ +case class CanDeleteListResponseADM(listIri: IRI, canDeleteList: Boolean) extends ListItemDeleteResponseADM { + + def toJsValue: JsValue = canDeleteListResponseADMFormat.write(this) +} + /** * Responds to change of a child node's position by returning its parent node together with list of its children. * @@ -1320,4 +1340,6 @@ trait ListADMJsonProtocol extends SprayJsonSupport with DefaultJsonProtocol with jsonFormat(ChildNodeDeleteResponseADM, "node") implicit val listDeleteResponseADMFormat: RootJsonFormat[ListDeleteResponseADM] = jsonFormat(ListDeleteResponseADM, "iri", "deleted") + implicit val canDeleteListResponseADMFormat: RootJsonFormat[CanDeleteListResponseADM] = + jsonFormat(CanDeleteListResponseADM, "listIri", "canDeleteList") } 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 a795d20e86..52604fc1f3 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 @@ -85,6 +85,8 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde nodePositionChangeRequest(nodeIri, changeNodePositionRequest, featureFactoryConfig, requestingUser, apiRequestID) case ListItemDeleteRequestADM(nodeIri, featureFactoryConfig, requestingUser, apiRequestID) => deleteListItemRequestADM(nodeIri, featureFactoryConfig, requestingUser, apiRequestID) + case CanDeleteListRequestADM(iri, featureFactoryConfig, requestingUser) => + canDeleteListRequestADM(iri) case other => handleUnexpectedMessage(other, log, this.getClass.getName) } @@ -1761,6 +1763,31 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde } yield taskResult } + /** + * Checks if a list can be deleted (none of its nodes is used in data). + */ + private def canDeleteListRequestADM( + iri: IRI + ): Future[CanDeleteListResponseADM] = + for { + sparqlQuery <- Future( + org.knora.webapi.messages.twirl.queries.sparql.admin.txt + .canDeleteList( + triplestore = settings.triplestoreType, + listIri = iri + ) + .toString() + ) + + response: SparqlSelectResult <- (storeManager ? SparqlSelectRequest(sparqlQuery)) + .mapTo[SparqlSelectResult] + + canDelete = + if (response.results.bindings.isEmpty) true + else false + + } yield CanDeleteListResponseADM(iri, canDelete) + /** * Delete a node (root or child). If a root node is given, check for its usage in data and ontology. If not used, * delete the list and return a confirmation message. 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 144b1e64aa..bbf2a25da3 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 @@ -6,7 +6,6 @@ package org.knora.webapi.routing.admin.lists import java.util.UUID - import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} import org.knora.webapi.exceptions.BadRequestException @@ -34,7 +33,8 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) import DeleteListItemsRouteADM._ def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = - deleteListItem(featureFactoryConfig) + deleteListItem(featureFactoryConfig) ~ + canDeleteList(featureFactoryConfig) /* delete list (i.e. root node) or a child node which should also delete its children */ private def deleteListItem(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => @@ -63,4 +63,32 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) ) } } + + /** + * Checks if a list can be deleted (none of its nodes is used in data). + */ + private def canDeleteList(featureFactoryConfig: FeatureFactoryConfig): Route = + path(ListsBasePath / "candelete" / Segment) { iri => + get { requestContext => + val listIri = + stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid list IRI: $iri")) + + val requestMessage: Future[CanDeleteListRequestADM] = for { + requestingUser <- getUserADM(requestContext, featureFactoryConfig) + } yield CanDeleteListRequestADM( + iri = listIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } } diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/canDeleteList.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/canDeleteList.scala.txt new file mode 100644 index 0000000000..867dd27008 --- /dev/null +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/canDeleteList.scala.txt @@ -0,0 +1,29 @@ +@* + * Copyright © 2021 Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + *@ + +@import org.knora.webapi.IRI + +@** + * Checks if a list can be deleted (none of its nodes is used in data). + * + * @param triplestore the name of the triplestore being used. + * @param listIri the IRI of the list to be checked. + *@ +@(triplestore: String, + listIri: IRI) + +PREFIX knora-base: + +SELECT DISTINCT ?isUsed + +WHERE { + BIND(IRI("@listIri") AS ?listToBeChecked) + BIND(true AS ?isUsed) + + { + ?listToBeChecked knora-base:hasSubListNode* ?childNode . + ?valueUsingNode knora-base:valueHasListNode ?childNode . + } +} 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 16dc215547..02add1e75f 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 @@ -166,6 +166,52 @@ class DeleteListItemsRouteADME2ESpec ) ) } + } + + "Candeletelist route (/admin/lists/candelete)" when { + "used to query if list can be deleted" should { + "return TRUE for unused list" in { + val unusedList = "http://rdfh.ch/lists/0001/notUsedList" + val unusedListEncoded = java.net.URLEncoder.encode(unusedList, "utf-8") + val request = Get(baseApiUrl + s"/admin/lists/candelete/" + unusedListEncoded) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password) + ) + + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.OK) + + val canDelete = AkkaHttpUtils.httpResponseToJson(response).fields("canDeleteList") + canDelete.convertTo[Boolean] should be(true) + val listIri = AkkaHttpUtils.httpResponseToJson(response).fields("listIri") + listIri.convertTo[String] should be(unusedList) + } + + "return FALSE for used list" in { + val usedList = "http://rdfh.ch/lists/0001/treeList01" + val usedListEncoded = java.net.URLEncoder.encode(usedList, "utf-8") + val request = Get(baseApiUrl + s"/admin/lists/candelete/" + usedListEncoded) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password) + ) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.OK) + + val canDelete = AkkaHttpUtils.httpResponseToJson(response).fields("canDeleteList") + canDelete.convertTo[Boolean] should be(false) + val listIri = AkkaHttpUtils.httpResponseToJson(response).fields("listIri") + listIri.convertTo[String] should be(usedList) + } + + "return exception for bad list iri" in { + val badlistIri = "bad list Iri" + val badListIriEncoded = java.net.URLEncoder.encode(badlistIri, "utf-8") + val request = Get(baseApiUrl + s"/admin/lists/candelete/" + badListIriEncoded) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password) + ) + + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.BadRequest) + } + } } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala index 17c3b8e51a..bc9d5e8ee5 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala @@ -944,5 +944,79 @@ class ListsResponderADMSpec extends CoreSpec(ListsResponderADMSpec.config) with received.deleted should be(true) } } + + "used to query if list can be deleted" should { + "return FALSE for a node that is in use" in { + val nodeInUseIri = "http://rdfh.ch/lists/0001/treeList01" + responderManager ! CanDeleteListRequestADM( + iri = nodeInUseIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: CanDeleteListResponseADM = expectMsgType[CanDeleteListResponseADM](timeout) + response.listIri should be(nodeInUseIri) + response.canDeleteList should be(false) + } + + "return FALSE for a node that is unused but has a child which is used" in { + val nodeIri = "http://rdfh.ch/lists/0001/treeList03" + responderManager ! CanDeleteListRequestADM( + iri = nodeIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: CanDeleteListResponseADM = expectMsgType[CanDeleteListResponseADM](timeout) + response.listIri should be(nodeIri) + response.canDeleteList should be(false) + } + + "return FALSE for a node used as object of salsah-gui:guiAttribute (i.e. 'hlist=') but not as object of knora-base:valueHasListNode" in { + val nodeInUseInOntologyIri = "http://rdfh.ch/lists/0001/treeList" + responderManager ! CanDeleteListRequestADM( + iri = nodeInUseInOntologyIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: CanDeleteListResponseADM = expectMsgType[CanDeleteListResponseADM](timeout) + response.listIri should be(nodeInUseInOntologyIri) + response.canDeleteList should be(false) + } + + "return TRUE for a middle child node that is not in use" in { + val nodeIri = "http://rdfh.ch/lists/0001/notUsedList012" + responderManager ! CanDeleteListRequestADM( + iri = nodeIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: CanDeleteListResponseADM = expectMsgType[CanDeleteListResponseADM](timeout) + response.listIri should be(nodeIri) + response.canDeleteList should be(true) + } + + "retrun TRUE for a child node that is not in use" in { + val nodeIri = "http://rdfh.ch/lists/0001/notUsedList02" + responderManager ! CanDeleteListRequestADM( + iri = nodeIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: CanDeleteListResponseADM = expectMsgType[CanDeleteListResponseADM](timeout) + response.listIri should be(nodeIri) + response.canDeleteList should be(true) + } + + "delete a list (i.e. root node) that is not in use in ontology" in { + val listIri = "http://rdfh.ch/lists/0001/notUsedList" + responderManager ! CanDeleteListRequestADM( + iri = listIri, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = SharedTestDataADM.anythingAdminUser + ) + val response: CanDeleteListResponseADM = expectMsgType[CanDeleteListResponseADM](timeout) + response.listIri should be(listIri) + response.canDeleteList should be(true) + } + } } }