Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
mpro7 and subotic committed Dec 17, 2021
1 parent c356f0c commit c276625
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 2 deletions.
Expand Up @@ -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

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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")
}
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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.
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -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
)
}
}
}
@@ -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: <http://www.knora.org/ontology/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 .
}
}
Expand Up @@ -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)
}
}
}
}
Expand Up @@ -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=<nodeIri>') 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)
}
}
}
}

0 comments on commit c276625

Please sign in to comment.