Commit bc01144d authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Implemented multiple nodes deletion #3826

parent 6c1a3374
Loading
Loading
Loading
Loading
Loading
+30 −8
Original line number Diff line number Diff line
@@ -5,7 +5,11 @@ import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.ListNodeData;
import it.inaf.ia2.vospace.ui.service.NodesService;
import it.inaf.oats.vospace.datamodel.NodeUtils;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Property;
@@ -19,7 +23,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -51,13 +54,6 @@ public class NodesController extends BaseController {
        return ResponseEntity.ok(nodesService.generateNodesHtml(path, principal));
    }

    @DeleteMapping(value = {"/nodes", "/nodes/**"})
    public void deleteNode() {
        String path = getPath("/nodes/");
        LOG.debug("deleteNode called for path {}", path);
        client.deleteNode(path);
    }

    @GetMapping(value = "/download/**")
    public ResponseEntity<?> directDownload() {

@@ -100,6 +96,32 @@ public class NodesController extends BaseController {
        client.createNode(node);
    }

    @PostMapping(value = "/delete", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> deleteNodes(@RequestBody List<String> nodesToDelete) {

        CompletableFuture[] deleteCalls = nodesToDelete.stream()
                .map(nodeToDelete -> {
                    return CompletableFuture.runAsync(() -> {
                        LOG.debug("deleteNode called for path {}", nodeToDelete);
                        client.deleteNode(nodeToDelete);
                    }, Runnable::run);
                })
                .collect(Collectors.toList())
                .toArray(CompletableFuture[]::new);

        Optional<RuntimeException> error = CompletableFuture.allOf(deleteCalls)
                .handle((fn, ex) -> {
                    return Optional.ofNullable(ex == null ? null : new RuntimeException(ex));
                }).join();

        if (error.isPresent()) {
            throw error.get();
        }

        // All the nodes have been correctly deleted
        return ResponseEntity.noContent().build();
    }
    
    protected String getPath(String prefix) {
        String requestURL = servletRequest.getRequestURL().toString();
        return NodeUtils.getPathFromRequestURLString(requestURL, prefix);
+5 −1
Original line number Diff line number Diff line
@@ -65,10 +65,14 @@ public class NodesService {
            return "";
        }

        boolean deletable = NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()) && !nodeInfo.isSticky();

        String html = "<tr>";
        html += "<td><input type=\"checkbox\" data-node=\"" + nodeInfo.getPath().replace("\"", "\\\"") + "\" ";
        if (nodeInfo.isAsyncTrans()) {
            html += "class=\"async\"";
        } else if (deletable) {
            html += "class=\"deletable\"";
        }
        html += "/></td>";
        html += "<td>" + getIcon(nodeInfo) + getLink(nodeInfo, user) + "</td>";
@@ -76,7 +80,7 @@ public class NodesService {
        html += "<td>" + nodeInfo.getGroupRead() + "</td>";
        html += "<td>" + nodeInfo.getGroupWrite() + "</td>";
        html += "<td>";
        if (NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()) && !nodeInfo.isSticky()) {
        if (deletable) {
            html += "<span class=\"icon trash-icon pointer\" onclick=\"deleteNode('" + nodeInfo.getPath() + "')\"></span>";
        }
        html += "</td>";
+49 −0
Original line number Diff line number Diff line
package it.inaf.ia2.vospace.ui.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.service.NodesService;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import org.mockito.Mockito;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.web.util.NestedServletException;

@SpringBootTest
@AutoConfigureMockMvc
public class NodesControllerTest {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @MockBean
    private NodesService nodesService;

    @MockBean
    private VOSpaceClient client;

    @Autowired
    private MockMvc mockMvc;

@@ -49,4 +65,37 @@ public class NodesControllerTest {

        verify(nodesService).generateNodesHtml(eq("/a/b/c"), any());
    }

    @Test
    public void testDeleteMultipleNodes() throws Exception {

        List<String> paths = Arrays.asList("/a/b/c", "/e/f/g");

        mockMvc.perform(post("/delete")
                .contentType(MediaType.APPLICATION_JSON)
                .content(MAPPER.writeValueAsString(paths)))
                .andExpect(status().isNoContent());

        verify(client, times(1)).deleteNode(eq("/a/b/c"));
        verify(client, times(1)).deleteNode(eq("/e/f/g"));
    }

    @Test
    public void testErrorOnDelete() throws Exception {

        doThrow(new RuntimeException())
                .when(client).deleteNode(any());

        boolean exception = false;
        try {
            mockMvc.perform(post("/delete")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(MAPPER.writeValueAsString(Arrays.asList("/test"))))
                    .andReturn();
        } catch (NestedServletException ex) {
            exception = true;
        }

        assertTrue(exception);
    }
}
+1 −1
Original line number Diff line number Diff line
{
  "writable": true,
  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" data-node=\"/folder1\" /></td>    <td>      <span class=\"icon folder-icon\"></span>      <a href=\"#/nodes/folder1\">folder1</a>    </td>    <td>0 B</td>    <td>group1</td>    <td>group2</td>    <td>      <span class=\"icon trash-icon pointer\" onclick=\"deleteNode('/folder1')\"></span>    </td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/file1\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file1\">file1</a>    </td>    <td>12 MB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr></tbody>"
  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"deletable\" data-node=\"/folder1\" /></td>    <td>      <span class=\"icon folder-icon\"></span>      <a href=\"#/nodes/folder1\">folder1</a>    </td>    <td>0 B</td>    <td>group1</td>    <td>group2</td>    <td>      <span class=\"icon trash-icon pointer\" onclick=\"deleteNode('/folder1')\"></span>    </td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/file1\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file1\">file1</a>    </td>    <td>12 MB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr></tbody>"
}
+2 −2
Original line number Diff line number Diff line
@@ -55,7 +55,7 @@ export default {
  uploadFile() {
    return fetch({});
  },
  deleteNode() {
  deleteNodes() {
    return fetch({});
  },
  keepalive() {
Loading