Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +30 −8 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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() { Loading Loading @@ -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); Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java +5 −1 Original line number Diff line number Diff line Loading @@ -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>"; Loading @@ -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>"; Loading vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java +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; Loading Loading @@ -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); } } vospace-ui-frontend/src/api/mock/data/nodes/root.json +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>" } vospace-ui-frontend/src/api/mock/index.js +2 −2 Original line number Diff line number Diff line Loading @@ -55,7 +55,7 @@ export default { uploadFile() { return fetch({}); }, deleteNode() { deleteNodes() { return fetch({}); }, keepalive() { Loading Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +30 −8 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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() { Loading Loading @@ -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); Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java +5 −1 Original line number Diff line number Diff line Loading @@ -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>"; Loading @@ -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>"; Loading
vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java +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; Loading Loading @@ -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); } }
vospace-ui-frontend/src/api/mock/data/nodes/root.json +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>" }
vospace-ui-frontend/src/api/mock/index.js +2 −2 Original line number Diff line number Diff line Loading @@ -55,7 +55,7 @@ export default { uploadFile() { return fetch({}); }, deleteNode() { deleteNodes() { return fetch({}); }, keepalive() { Loading