Commit 5b9e1610 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Changed backend for handling copy and move of multiple selected nodes

parent 0341cfd6
Loading
Loading
Loading
Loading
+79 −32
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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();

@@ -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);
@@ -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) {
+2 −1
Original line number Diff line number Diff line
@@ -16,7 +16,8 @@ public class Job {
    public static enum JobType {
        ASYNC_RECALL,
        ARCHIVE,
        MOVE
        MOVE,
        COPY
    }

    private String id;
+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;
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -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");
+8 −7
Original line number Diff line number Diff line
@@ -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;
    }
@@ -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;
        }

@@ -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