Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(listsADM): add canDeleteList route #1968

Merged
merged 8 commits into from Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
*
* @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,29 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde
} yield taskResult
}

private def canDeleteListRequestADM(
iri: IRI
): Future[CanDeleteListResponseADM] = {
var canDelete = false
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
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]

_ = if (response.results.bindings.isEmpty) {
canDelete = true
}
} 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 (and its children) is unused and if can be deleted.
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
*/
private def canDeleteList(featureFactoryConfig: FeatureFactoryConfig): Route =
path(ListsBasePath / "candelete" / Segment) { iri =>
get { requestContext =>
val listIri =
stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri"))
mpro7 marked this conversation as resolved.
Show resolved Hide resolved

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 (and its children) is unused and if can be deleted.
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
*
* @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 ?nodeToBeDeleted)
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
BIND(true AS ?isUsed)

{
?nodeToBeDeleted knora-base:hasSubListNode* ?subNode .
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
?valueUsingNode knora-base:valueHasListNode ?subNode .
}
}
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 unused list" in {
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
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 {
mpro7 marked this conversation as resolved.
Show resolved Hide resolved
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)
}

"retrn 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)
}
}
}
}