Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +79 −32 Original line number Diff line number Diff line Loading @@ -9,16 +9,20 @@ import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.data.ListNodeData; import it.inaf.ia2.vospace.ui.data.MoveOrCopyRequest; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator; import it.inaf.ia2.vospace.ui.service.MoveNodeModalHtmlGenerator; import it.inaf.ia2.vospace.ui.service.MoveOrCopyNodeModalHtmlGenerator; import it.inaf.oats.vospace.datamodel.NodeUtils; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; Loading Loading @@ -49,8 +53,8 @@ public class NodesController extends BaseController { @Value("${vospace-authority}") private String authority; @Value("${maxPollingAttempts:10}") private int maxPollingAttempts; @Value("${pollingTimeout:15}") private int pollingTimeout; @Autowired private VOSpaceClient client; Loading @@ -76,10 +80,10 @@ public class NodesController extends BaseController { return ResponseEntity.ok(listNodeData); } @GetMapping(value = "/nodesForMove", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<ListNodeData> listNodesForMoveModal(@RequestParam("path") String path, @RequestParam("nodeToMove") String nodeToMove, User principal) throws Exception { @GetMapping(value = "/nodesForMoveOrCopy", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<ListNodeData> listNodesForMoveOrCopyModal(@RequestParam("path") String path, @RequestParam("target") List<String> targetNodes, User principal) throws Exception { LOG.debug("listNodes called for path {}", path); LOG.debug("nodesForMoveOrCopy called for path {}", path); ListNodeData listNodeData = new ListNodeData(); Loading @@ -87,7 +91,7 @@ public class NodesController extends BaseController { listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups())); MoveNodeModalHtmlGenerator htmlGenerator = new MoveNodeModalHtmlGenerator(node, nodeToMove, principal, authority); MoveOrCopyNodeModalHtmlGenerator htmlGenerator = new MoveOrCopyNodeModalHtmlGenerator(node, targetNodes, principal, authority); listNodeData.setHtml(htmlGenerator.generateNodes()); return ResponseEntity.ok(listNodeData); Loading Loading @@ -203,40 +207,83 @@ public class NodesController extends BaseController { return commonParent; } @PostMapping(value = "/move") public ResponseEntity<Job> moveNode(@RequestBody Map<String, Object> params) { String target = urlEncodePath(getRequiredParam(params, "target")); String direction = urlEncodePath(getRequiredParam(params, "direction")); @PostMapping(value = "/moveOrCopy") public ResponseEntity<List<Job>> moveOrCopyNodes(@RequestBody MoveOrCopyRequest request) throws Exception { CompletableFuture<JobSummary>[] futureJobs = request.getTargets().stream().map(t -> { String target = urlEncodePath(t); String direction = urlEncodePath(request.getDirection()); Transfer transfer = new Transfer(); transfer.setTarget("vos://" + authority + target); transfer.setDirection("vos://" + authority + direction); transfer.setKeepBytes(request.isKeepBytes()); return CompletableFuture.supplyAsync(() -> client.startTransferJob(transfer), Runnable::run); }).collect(Collectors.toList()).toArray(CompletableFuture[]::new); CompletableFuture.allOf(futureJobs).join(); List<JobSummary> jobs = Stream.of(futureJobs).map(j -> j.join()).collect(Collectors.toList()); AtomicReference<Boolean> cancelled = new AtomicReference<>(false); CompletableFuture timeout = CompletableFuture.runAsync(() -> { try { Thread.sleep(pollingTimeout * 1000); } catch (InterruptedException ex) { } cancelled.set(true); }); JobSummary job = client.startTransferJob(transfer); try { CompletableFuture.anyOf(jobsPolling(jobs, cancelled), timeout).join(); } catch (CompletionException ex) { if (ex.getCause() != null && ex.getCause() instanceof VOSpaceException) { throw (VOSpaceException) ex.getCause(); } throw ex; } // Try to perform polling until completion. If it takes too much time // sends the execution phase to the UI and let it handles the polling. ExecutionPhase phase; int i = 0; do { phase = client.getJobPhase(job.getJobId()); if (phase == ExecutionPhase.COMPLETED) { break; } else if (phase == ExecutionPhase.ERROR) { String errorDetail = client.getErrorDetail(job.getJobId()); throw new VOSpaceException("MoveNode operation failed: " + errorDetail); Job.JobType type = request.isKeepBytes() ? Job.JobType.COPY : Job.JobType.MOVE; return ResponseEntity.ok(jobs.stream().map(j -> new Job(j, type)) .collect(Collectors.toList())); } private CompletableFuture<?> jobsPolling(List<JobSummary> jobs, AtomicReference<Boolean> cancelled) { return CompletableFuture.runAsync(() -> { List<JobSummary> uncompletedJobs; do { uncompletedJobs = jobs.stream() .filter(j -> ExecutionPhase.COMPLETED != j.getPhase()) .collect(Collectors.toList()); if (!uncompletedJobs.isEmpty()) { try { Thread.sleep(1000); } catch (InterruptedException ex) { break; } updatePhases(uncompletedJobs); } } while (!uncompletedJobs.isEmpty() && !cancelled.get()); }, Runnable::run); } i++; } while (i < maxPollingAttempts); private void updatePhases(List<JobSummary> uncompletedJobs) { CompletableFuture[] phaseFutures = uncompletedJobs.stream() .map(job -> CompletableFuture.runAsync(() -> { ExecutionPhase phase = client.getJobPhase(job.getJobId()); if (phase == ExecutionPhase.ERROR) { String errorDetail = client.getErrorDetail(job.getJobId()); throw new VOSpaceException("Operation failed: " + errorDetail); } job.setPhase(phase); }, Runnable::run)).collect(Collectors.toList()).toArray(CompletableFuture[]::new); return ResponseEntity.ok(new Job(job, Job.JobType.MOVE)); CompletableFuture.allOf(phaseFutures).join(); } protected String getPath(String prefix) { Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java +2 −1 Original line number Diff line number Diff line Loading @@ -16,7 +16,8 @@ public class Job { public static enum JobType { ASYNC_RECALL, ARCHIVE, MOVE MOVE, COPY } private String id; Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/MoveOrCopyRequest.java 0 → 100644 +39 −0 Original line number Diff line number Diff line /* * This file is part of vospace-ui * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.ia2.vospace.ui.data; import java.util.List; public class MoveOrCopyRequest { private List<String> targets; private String direction; private boolean keepBytes; public List<String> getTargets() { return targets; } public void setTargets(List<String> targets) { this.targets = targets; } public String getDirection() { return direction; } public void setDirection(String direction) { this.direction = direction; } public boolean isKeepBytes() { return keepBytes; } public void setKeepBytes(boolean keepBytes) { this.keepBytes = keepBytes; } } vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java +6 −0 Original line number Diff line number Diff line Loading @@ -137,6 +137,12 @@ public class MainNodesHtmlGenerator extends NodesHtmlGenerator { moveBtn.attr("class", "dropdown-item"); moveBtn.attr("onclick", "moveNode(" + nodePathJs + ")"); Element copyBtn = dropdown.appendElement("button"); copyBtn.text("Copy"); copyBtn.attr("type", "button"); copyBtn.attr("class", "dropdown-item"); copyBtn.attr("onclick", "copyNode(" + nodePathJs + ")"); Element deleteBtn = dropdown.appendElement("button"); deleteBtn.text("Delete"); deleteBtn.attr("type", "button"); Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MoveNodeModalHtmlGenerator.java→vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MoveOrCopyNodeModalHtmlGenerator.java +8 −7 Original line number Diff line number Diff line Loading @@ -6,23 +6,24 @@ package it.inaf.ia2.vospace.ui.service; import it.inaf.ia2.aa.data.User; import java.util.List; import net.ivoa.xml.vospace.v2.Node; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; public class MoveNodeModalHtmlGenerator extends NodesHtmlGenerator { public class MoveOrCopyNodeModalHtmlGenerator extends NodesHtmlGenerator { private final String nodeNodeMovePath; private final List<String> targetNodes; public MoveNodeModalHtmlGenerator(Node node, String nodeNodeMovePath, User user, String authority) { public MoveOrCopyNodeModalHtmlGenerator(Node node, List<String> targetNodes, User user, String authority) { super(node, user, authority); this.nodeNodeMovePath = nodeNodeMovePath; this.targetNodes = targetNodes; } @Override protected Element createContainerElement(Document html) { Element container = html.body().appendElement("div"); container.attr("id", "move-nodes"); container.attr("id", "move-or-copy-nodes"); container.attr("class", "list-group"); return container; } Loading @@ -31,7 +32,7 @@ public class MoveNodeModalHtmlGenerator extends NodesHtmlGenerator { protected void addChild(Node child, Element containerElement) { NodeInfo nodeInfo = new NodeInfo(child, user, authority); if (!nodeInfo.isFolder() || nodeInfo.getPath().equals(nodeNodeMovePath)) { if (!nodeInfo.isFolder() || targetNodes.contains(nodeInfo.getPath())) { return; } Loading @@ -45,7 +46,7 @@ public class MoveNodeModalHtmlGenerator extends NodesHtmlGenerator { private void addLink(NodeInfo nodeInfo, Element cell) { Element link = cell.appendElement("a"); link.attr("href", "#"); link.attr("onclick", "openNodeInMoveModal(event, " + makeJsArg(nodeInfo.getPath()) + ")"); link.attr("onclick", "openNodeInMoveOrCopyModal(event, " + makeJsArg(nodeInfo.getPath()) + ")"); link.text(nodeInfo.getName()); } } Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +79 −32 Original line number Diff line number Diff line Loading @@ -9,16 +9,20 @@ import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.data.ListNodeData; import it.inaf.ia2.vospace.ui.data.MoveOrCopyRequest; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator; import it.inaf.ia2.vospace.ui.service.MoveNodeModalHtmlGenerator; import it.inaf.ia2.vospace.ui.service.MoveOrCopyNodeModalHtmlGenerator; import it.inaf.oats.vospace.datamodel.NodeUtils; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; Loading Loading @@ -49,8 +53,8 @@ public class NodesController extends BaseController { @Value("${vospace-authority}") private String authority; @Value("${maxPollingAttempts:10}") private int maxPollingAttempts; @Value("${pollingTimeout:15}") private int pollingTimeout; @Autowired private VOSpaceClient client; Loading @@ -76,10 +80,10 @@ public class NodesController extends BaseController { return ResponseEntity.ok(listNodeData); } @GetMapping(value = "/nodesForMove", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<ListNodeData> listNodesForMoveModal(@RequestParam("path") String path, @RequestParam("nodeToMove") String nodeToMove, User principal) throws Exception { @GetMapping(value = "/nodesForMoveOrCopy", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<ListNodeData> listNodesForMoveOrCopyModal(@RequestParam("path") String path, @RequestParam("target") List<String> targetNodes, User principal) throws Exception { LOG.debug("listNodes called for path {}", path); LOG.debug("nodesForMoveOrCopy called for path {}", path); ListNodeData listNodeData = new ListNodeData(); Loading @@ -87,7 +91,7 @@ public class NodesController extends BaseController { listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups())); MoveNodeModalHtmlGenerator htmlGenerator = new MoveNodeModalHtmlGenerator(node, nodeToMove, principal, authority); MoveOrCopyNodeModalHtmlGenerator htmlGenerator = new MoveOrCopyNodeModalHtmlGenerator(node, targetNodes, principal, authority); listNodeData.setHtml(htmlGenerator.generateNodes()); return ResponseEntity.ok(listNodeData); Loading Loading @@ -203,40 +207,83 @@ public class NodesController extends BaseController { return commonParent; } @PostMapping(value = "/move") public ResponseEntity<Job> moveNode(@RequestBody Map<String, Object> params) { String target = urlEncodePath(getRequiredParam(params, "target")); String direction = urlEncodePath(getRequiredParam(params, "direction")); @PostMapping(value = "/moveOrCopy") public ResponseEntity<List<Job>> moveOrCopyNodes(@RequestBody MoveOrCopyRequest request) throws Exception { CompletableFuture<JobSummary>[] futureJobs = request.getTargets().stream().map(t -> { String target = urlEncodePath(t); String direction = urlEncodePath(request.getDirection()); Transfer transfer = new Transfer(); transfer.setTarget("vos://" + authority + target); transfer.setDirection("vos://" + authority + direction); transfer.setKeepBytes(request.isKeepBytes()); return CompletableFuture.supplyAsync(() -> client.startTransferJob(transfer), Runnable::run); }).collect(Collectors.toList()).toArray(CompletableFuture[]::new); CompletableFuture.allOf(futureJobs).join(); List<JobSummary> jobs = Stream.of(futureJobs).map(j -> j.join()).collect(Collectors.toList()); AtomicReference<Boolean> cancelled = new AtomicReference<>(false); CompletableFuture timeout = CompletableFuture.runAsync(() -> { try { Thread.sleep(pollingTimeout * 1000); } catch (InterruptedException ex) { } cancelled.set(true); }); JobSummary job = client.startTransferJob(transfer); try { CompletableFuture.anyOf(jobsPolling(jobs, cancelled), timeout).join(); } catch (CompletionException ex) { if (ex.getCause() != null && ex.getCause() instanceof VOSpaceException) { throw (VOSpaceException) ex.getCause(); } throw ex; } // Try to perform polling until completion. If it takes too much time // sends the execution phase to the UI and let it handles the polling. ExecutionPhase phase; int i = 0; do { phase = client.getJobPhase(job.getJobId()); if (phase == ExecutionPhase.COMPLETED) { break; } else if (phase == ExecutionPhase.ERROR) { String errorDetail = client.getErrorDetail(job.getJobId()); throw new VOSpaceException("MoveNode operation failed: " + errorDetail); Job.JobType type = request.isKeepBytes() ? Job.JobType.COPY : Job.JobType.MOVE; return ResponseEntity.ok(jobs.stream().map(j -> new Job(j, type)) .collect(Collectors.toList())); } private CompletableFuture<?> jobsPolling(List<JobSummary> jobs, AtomicReference<Boolean> cancelled) { return CompletableFuture.runAsync(() -> { List<JobSummary> uncompletedJobs; do { uncompletedJobs = jobs.stream() .filter(j -> ExecutionPhase.COMPLETED != j.getPhase()) .collect(Collectors.toList()); if (!uncompletedJobs.isEmpty()) { try { Thread.sleep(1000); } catch (InterruptedException ex) { break; } updatePhases(uncompletedJobs); } } while (!uncompletedJobs.isEmpty() && !cancelled.get()); }, Runnable::run); } i++; } while (i < maxPollingAttempts); private void updatePhases(List<JobSummary> uncompletedJobs) { CompletableFuture[] phaseFutures = uncompletedJobs.stream() .map(job -> CompletableFuture.runAsync(() -> { ExecutionPhase phase = client.getJobPhase(job.getJobId()); if (phase == ExecutionPhase.ERROR) { String errorDetail = client.getErrorDetail(job.getJobId()); throw new VOSpaceException("Operation failed: " + errorDetail); } job.setPhase(phase); }, Runnable::run)).collect(Collectors.toList()).toArray(CompletableFuture[]::new); return ResponseEntity.ok(new Job(job, Job.JobType.MOVE)); CompletableFuture.allOf(phaseFutures).join(); } protected String getPath(String prefix) { Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java +2 −1 Original line number Diff line number Diff line Loading @@ -16,7 +16,8 @@ public class Job { public static enum JobType { ASYNC_RECALL, ARCHIVE, MOVE MOVE, COPY } private String id; Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/MoveOrCopyRequest.java 0 → 100644 +39 −0 Original line number Diff line number Diff line /* * This file is part of vospace-ui * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.ia2.vospace.ui.data; import java.util.List; public class MoveOrCopyRequest { private List<String> targets; private String direction; private boolean keepBytes; public List<String> getTargets() { return targets; } public void setTargets(List<String> targets) { this.targets = targets; } public String getDirection() { return direction; } public void setDirection(String direction) { this.direction = direction; } public boolean isKeepBytes() { return keepBytes; } public void setKeepBytes(boolean keepBytes) { this.keepBytes = keepBytes; } }
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java +6 −0 Original line number Diff line number Diff line Loading @@ -137,6 +137,12 @@ public class MainNodesHtmlGenerator extends NodesHtmlGenerator { moveBtn.attr("class", "dropdown-item"); moveBtn.attr("onclick", "moveNode(" + nodePathJs + ")"); Element copyBtn = dropdown.appendElement("button"); copyBtn.text("Copy"); copyBtn.attr("type", "button"); copyBtn.attr("class", "dropdown-item"); copyBtn.attr("onclick", "copyNode(" + nodePathJs + ")"); Element deleteBtn = dropdown.appendElement("button"); deleteBtn.text("Delete"); deleteBtn.attr("type", "button"); Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MoveNodeModalHtmlGenerator.java→vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MoveOrCopyNodeModalHtmlGenerator.java +8 −7 Original line number Diff line number Diff line Loading @@ -6,23 +6,24 @@ package it.inaf.ia2.vospace.ui.service; import it.inaf.ia2.aa.data.User; import java.util.List; import net.ivoa.xml.vospace.v2.Node; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; public class MoveNodeModalHtmlGenerator extends NodesHtmlGenerator { public class MoveOrCopyNodeModalHtmlGenerator extends NodesHtmlGenerator { private final String nodeNodeMovePath; private final List<String> targetNodes; public MoveNodeModalHtmlGenerator(Node node, String nodeNodeMovePath, User user, String authority) { public MoveOrCopyNodeModalHtmlGenerator(Node node, List<String> targetNodes, User user, String authority) { super(node, user, authority); this.nodeNodeMovePath = nodeNodeMovePath; this.targetNodes = targetNodes; } @Override protected Element createContainerElement(Document html) { Element container = html.body().appendElement("div"); container.attr("id", "move-nodes"); container.attr("id", "move-or-copy-nodes"); container.attr("class", "list-group"); return container; } Loading @@ -31,7 +32,7 @@ public class MoveNodeModalHtmlGenerator extends NodesHtmlGenerator { protected void addChild(Node child, Element containerElement) { NodeInfo nodeInfo = new NodeInfo(child, user, authority); if (!nodeInfo.isFolder() || nodeInfo.getPath().equals(nodeNodeMovePath)) { if (!nodeInfo.isFolder() || targetNodes.contains(nodeInfo.getPath())) { return; } Loading @@ -45,7 +46,7 @@ public class MoveNodeModalHtmlGenerator extends NodesHtmlGenerator { private void addLink(NodeInfo nodeInfo, Element cell) { Element link = cell.appendElement("a"); link.attr("href", "#"); link.attr("onclick", "openNodeInMoveModal(event, " + makeJsArg(nodeInfo.getPath()) + ")"); link.attr("onclick", "openNodeInMoveOrCopyModal(event, " + makeJsArg(nodeInfo.getPath()) + ")"); link.text(nodeInfo.getName()); } }