Skip to content
Snippets Groups Projects
Commit fbbe963a authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Supported link nodes in node listing

parent d5a6e295
No related branches found
No related tags found
No related merge requests found
Pipeline #2331 passed
Showing
with 199 additions and 22 deletions
......@@ -15,6 +15,7 @@ import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator;
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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
......@@ -27,6 +28,7 @@ import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.uws.v1.ExecutionPhase;
import net.ivoa.xml.uws.v1.JobSummary;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.LinkNode;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Param;
import net.ivoa.xml.vospace.v2.Property;
......@@ -74,7 +76,9 @@ public class NodesController extends BaseController {
listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups()));
MainNodesHtmlGenerator htmlGenerator = new MainNodesHtmlGenerator(node, principal, authority);
List<Node> linkedNodes = getLinkedNodes(node, false);
MainNodesHtmlGenerator htmlGenerator = new MainNodesHtmlGenerator(node, principal, authority, linkedNodes);
listNodeData.setHtml(htmlGenerator.generateNodes());
return ResponseEntity.ok(listNodeData);
......@@ -91,12 +95,55 @@ public class NodesController extends BaseController {
listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups()));
MoveOrCopyNodeModalHtmlGenerator htmlGenerator = new MoveOrCopyNodeModalHtmlGenerator(node, targetNodes, principal, authority);
List<Node> linkedNodes = getLinkedNodes(node, true);
MoveOrCopyNodeModalHtmlGenerator htmlGenerator = new MoveOrCopyNodeModalHtmlGenerator(node, targetNodes, principal, authority, linkedNodes);
listNodeData.setHtml(htmlGenerator.generateNodes());
return ResponseEntity.ok(listNodeData);
}
private List<Node> getLinkedNodes(Node node, boolean onlyDirectories) throws Exception {
List<Node> linkedNodes = new ArrayList<>();
if (node instanceof ContainerNode) {
ContainerNode container = (ContainerNode) node;
List<LinkNode> linksToLoad = container.getNodes().stream().filter(n -> n instanceof LinkNode)
.map(n -> (LinkNode) n).collect(Collectors.toList());
// it is necessary to load link nodes metadata to understand if the linked node represents
// a container, a VOSpace file or an external file
List<CompletableFuture<Node>> nodesCalls = new ArrayList<>();
for (LinkNode linkNode : linksToLoad) {
String prefix = "vos://" + authority;
if (linkNode.getTarget().startsWith(prefix)) {
String linkPath = linkNode.getTarget().substring(prefix.length());
nodesCalls.add(CompletableFuture.supplyAsync(() -> client.getNode(linkPath), Runnable::run)
.exceptionally(ex -> null)); // null is returned in case of broken link
} else {
linkedNodes.add(linkNode);
}
}
CompletableFuture.allOf(nodesCalls.toArray(CompletableFuture[]::new)).join();
for (CompletableFuture<Node> nodeCall : nodesCalls) {
Node linkedNode = nodeCall.get();
if (linkedNode != null) {
if (linkedNode instanceof ContainerNode) {
linkedNodes.add(linkedNode);
} else if (!onlyDirectories) {
linkedNodes.add(linkedNode);
}
}
}
}
return linkedNodes;
}
@PostMapping(value = "/folder")
public void newFolder(@RequestBody Map<String, Object> params) {
......
......@@ -16,8 +16,8 @@ import org.jsoup.nodes.Element;
public class MainNodesHtmlGenerator extends NodesHtmlGenerator {
public MainNodesHtmlGenerator(Node node, User user, String authority) {
super(node, user, authority);
public MainNodesHtmlGenerator(Node node, User user, String authority, List<Node> linkedNodes) {
super(node, user, authority, linkedNodes);
}
@Override
......@@ -29,7 +29,10 @@ public class MainNodesHtmlGenerator extends NodesHtmlGenerator {
@Override
protected void addChild(Node child, Element containerElement) {
NodeInfo nodeInfo = new NodeInfo(child, user, authority);
Node linkedNode = getLinkedNode(child);
NodeInfo nodeInfo = new NodeInfo(child, user, authority, linkedNode);
Element row = containerElement.appendElement("tr");
......
......@@ -7,6 +7,7 @@ package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.aa.data.User;
import java.util.List;
import net.ivoa.xml.vospace.v2.LinkNode;
import net.ivoa.xml.vospace.v2.Node;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
......@@ -15,8 +16,8 @@ public class MoveOrCopyNodeModalHtmlGenerator extends NodesHtmlGenerator {
private final List<String> targetNodes;
public MoveOrCopyNodeModalHtmlGenerator(Node node, List<String> targetNodes, User user, String authority) {
super(node, user, authority);
public MoveOrCopyNodeModalHtmlGenerator(Node node, List<String> targetNodes, User user, String authority, List<Node> linkedNodes) {
super(node, user, authority, linkedNodes);
this.targetNodes = targetNodes;
}
......@@ -30,7 +31,17 @@ public class MoveOrCopyNodeModalHtmlGenerator extends NodesHtmlGenerator {
@Override
protected void addChild(Node child, Element containerElement) {
NodeInfo nodeInfo = new NodeInfo(child, user, authority);
Node linkedNode = null;
if (child instanceof LinkNode) {
linkedNode = getLinkedNode(child);
if (linkedNode == null) {
// linked node is a file
return;
}
}
NodeInfo nodeInfo = new NodeInfo(child, user, authority, linkedNode);
if (!nodeInfo.isFolder() || targetNodes.contains(nodeInfo.getPath())) {
return;
......
......@@ -14,7 +14,9 @@ import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Collectors;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.LinkNode;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Property;
import org.slf4j.Logger;
......@@ -26,10 +28,10 @@ public class NodeInfo {
private final String authority;
private final String path;
private String path;
private final String name;
private final String size;
private final String type;
private String type;
private final String creator;
private final String groupRead;
private final String groupWrite;
......@@ -39,8 +41,9 @@ public class NodeInfo {
private final boolean busy;
private final boolean writable;
private final boolean deletable;
private final boolean link;
public NodeInfo(Node node, User user, String authority) {
public NodeInfo(Node node, User user, String authority, Node linkedNode) {
this.authority = authority;
this.path = getPath(node);
this.name = URLDecoder.decode(path.substring(path.lastIndexOf("/") + 1), StandardCharsets.UTF_8);
......@@ -55,6 +58,15 @@ public class NodeInfo {
this.busy = isBusy(node);
this.writable = NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()) && !busy;
this.deletable = writable && !sticky && !asyncTrans;
this.link = linkedNode != null;
if (linkedNode != null) {
String prefix = "vos://" + authority;
String target = ((LinkNode) node).getTarget();
if (linkedNode instanceof ContainerNode) {
this.path = decodePath(target, prefix);
}
this.type = linkedNode.getType();
}
}
private String getPath(Node node) {
......@@ -67,7 +79,11 @@ public class NodeInfo {
throw new VOSpaceException("Node authority is different from configured one! Configured is " + authority + ", but node URI is " + uri);
}
// returns decoded path
return decodePath(uri, prefix);
}
private String decodePath(String uri, String prefix) {
return String.join("/", Arrays.stream(uri.substring(prefix.length()).split("/"))
.map(p -> URLDecoder.decode(p, StandardCharsets.UTF_8))
.collect(Collectors.toList()));
......@@ -192,4 +208,8 @@ public class NodeInfo {
public boolean isDeletable() {
return deletable;
}
public boolean isLink() {
return link;
}
}
......@@ -6,7 +6,9 @@
package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.aa.data.User;
import java.util.List;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.LinkNode;
import net.ivoa.xml.vospace.v2.Node;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
......@@ -18,13 +20,15 @@ public abstract class NodesHtmlGenerator {
protected final User user;
protected final String authority;
private final List<Node> linkedNodes;
private Element containerElement;
protected NodesHtmlGenerator(Node node, User user, String authority) {
protected NodesHtmlGenerator(Node node, User user, String authority, List<Node> linkedNodes) {
this.parentNode = node;
this.user = user;
this.authority = authority;
this.linkedNodes = linkedNodes;
}
public String generateNodes() {
......@@ -67,7 +71,13 @@ public abstract class NodesHtmlGenerator {
Element icon = parentElement.appendElement("span");
icon.addClass("icon");
if (nodeInfo.isLink()) {
if (nodeInfo.isFolder()) {
icon.addClass("folder-link-icon");
} else {
icon.addClass("link-icon");
}
} else if (nodeInfo.isFolder()) {
if (nodeInfo.isAsyncTrans()) {
icon.addClass("folder-x-icon");
} else {
......@@ -83,4 +93,12 @@ public abstract class NodesHtmlGenerator {
parentElement.append("&nbsp;");
}
protected Node getLinkedNode(Node node) {
if (node instanceof LinkNode) {
return linkedNodes.stream().filter(n -> ((LinkNode) node)
.getTarget().equals(n.getUri())).findFirst().orElse(null);
}
return null;
}
}
......@@ -17,6 +17,7 @@ import net.ivoa.xml.uws.v1.ExecutionPhase;
import net.ivoa.xml.uws.v1.JobSummary;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.LinkNode;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
......@@ -145,10 +146,27 @@ public class NodesControllerTest {
parent.getNodes().add(child3);
ContainerNode child4 = new ContainerNode();
child4.setUri("vos://example.com!vospace/a/b/c/c3");
child4.setUri("vos://example.com!vospace/a/b/c/c4");
parent.getNodes().add(child4);
when(client.getNode(any())).thenReturn(parent);
LinkNode child5 = new LinkNode();
child5.setUri("vos://example.com!vospace/a/b/c/c5");
child5.setTarget("vos://example.com!vospace/a/b/c/c2");
parent.getNodes().add(child5);
LinkNode child6 = new LinkNode();
child6.setUri("vos://example.com!vospace/a/b/c/c6");
child6.setTarget("vos://example.com!vospace/a/b/c/c1");
parent.getNodes().add(child6);
LinkNode child7 = new LinkNode();
child7.setUri("vos://example.com!vospace/a/b/c/c7");
child7.setTarget("http://external-link");
parent.getNodes().add(child7);
when(client.getNode(eq("/a/b/c"))).thenReturn(parent);
when(client.getNode(eq("/a/b/c/c1"))).thenReturn(child1);
when(client.getNode(eq("/a/b/c/c2"))).thenReturn(child2);
String response = mockMvc.perform(get("/nodesForMoveOrCopy")
.param("path", "/a/b/c")
......@@ -161,6 +179,9 @@ public class NodesControllerTest {
assertFalse(response.contains("c2")); // data node
assertFalse(response.contains("c3")); // nodeToMove
assertFalse(response.contains("c4")); // nodeToMove
assertFalse(response.contains("c5")); // link to file
assertTrue(response.contains("c6")); // link to container
assertFalse(response.contains("c7")); // external link
verify(client, times(1)).getNode(eq("/a/b/c"));
}
......@@ -221,6 +242,34 @@ public class NodesControllerTest {
verify(client, times(1)).createNode(any());
}
@Test
public void testListLinkNodes() throws Exception {
ContainerNode parent = new ContainerNode();
parent.setUri("vos://example.com!vospace");
LinkNode link1 = new LinkNode();
link1.setUri("vos://example.com!vospace/link1");
link1.setTarget("vos://example.com!vospace/myfile");
parent.getNodes().add(link1);
LinkNode link2 = new LinkNode();
link2.setUri("vos://example.com!vospace/link2");
link2.setTarget("http://external-link");
parent.getNodes().add(link2);
DataNode linkedNode = new DataNode();
linkedNode.setUri("vos://example.com!vospace/myfile");
when(client.getNode(eq("/"))).thenReturn(parent);
when(client.getNode(eq("/myfile"))).thenReturn(linkedNode);
mockMvc.perform(get("/nodes"))
.andExpect(status().isOk());
verify(client, times(1)).getNode(eq("/"));
}
private ResultActions testMoveNode() throws Exception {
JobSummary job = new JobSummary();
......
......@@ -55,14 +55,14 @@ public class NodeInfoTest {
public void testUrlEncoding() {
DataNode node = new DataNode();
node.setUri("vos://example.com!vospace/my%23node");
NodeInfo nodeInfo = new NodeInfo(node, new User(), AUTHORITY);
NodeInfo nodeInfo = new NodeInfo(node, new User(), AUTHORITY, null);
assertEquals("my#node", nodeInfo.getName());
}
private void testNodeLength(long bytes, String expectedText) {
DataNode node = getDataNode();
setLength(node, bytes);
NodeInfo nodeInfo = new NodeInfo(node, new User(), AUTHORITY);
NodeInfo nodeInfo = new NodeInfo(node, new User(), AUTHORITY, null);
assertEquals(expectedText, nodeInfo.getSize());
}
......
......@@ -8,8 +8,10 @@ package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.aa.data.User;
import it.inaf.oats.vospace.datamodel.NodeProperties;
import java.util.Arrays;
import java.util.List;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.LinkNode;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Property;
import static org.junit.jupiter.api.Assertions.assertTrue;
......@@ -29,11 +31,31 @@ public class NodesHtmlGeneratorTest {
setGroups(file1, "group1", "group2 people.name\\.surname");
parent.getNodes().add(file1);
LinkNode link1 = new LinkNode();
link1.setUri("vos://example.com!vospace/mynode/link1");
link1.setTarget("vos://example.com!vospace/mynode");
setGroups(link1, "group1", "group2 people.name\\.surname");
parent.getNodes().add(link1);
LinkNode link2 = new LinkNode();
link2.setUri("vos://example.com!vospace/mynode/link2");
link2.setTarget("vos://example.com!vospace/mynode/file1");
setGroups(link2, "group1", "group2 people.name\\.surname");
parent.getNodes().add(link2);
LinkNode link3 = new LinkNode();
link3.setUri("vos://example.com!vospace/mynode/link3");
link3.setTarget("http://external-link");
setGroups(link3, "group1", "group2 people.name\\.surname");
parent.getNodes().add(link3);
List<Node> linkedNodes = List.of(parent, file1);
User user = new User();
user.setUserId("user_id");
user.setGroups(Arrays.asList("group1", "group2"));
MainNodesHtmlGenerator generator = new MainNodesHtmlGenerator(parent, user, "example.com!vospace");
MainNodesHtmlGenerator generator = new MainNodesHtmlGenerator(parent, user, "example.com!vospace", linkedNodes);
String html = generator.generateNodes();
......@@ -41,6 +63,9 @@ public class NodesHtmlGeneratorTest {
assertTrue(html.startsWith("<tbody id=\"nodes\">"));
assertTrue(html.contains("<input type=\"checkbox\" data-node=\"/mynode/file1\" class=\"deletable\">"));
assertTrue(html.contains("<span class=\"icon folder-link-icon\"></span>&nbsp;<a href=\"#/nodes/mynode\">link1</a>"));
assertTrue(html.contains("<span class=\"icon link-icon\"></span>&nbsp;<a target=\"blank_\" href=\"download/mynode/link2\">link2</a>"));
assertTrue(html.contains("<span class=\"icon file-icon\"></span>&nbsp;<a target=\"blank_\" href=\"download/mynode/link3\">link3</a>"));
}
private void setGroups(Node node, String groupRead, String groupWrite) {
......
......@@ -48,3 +48,7 @@
.dots-menu-icon {
background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='three dots vertical' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-three-dots-vertical mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
.link-icon {
background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='link45deg' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-link45deg mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M4.715 6.542L3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.001 1.001 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z'%3E%3C/path%3E%3Cpath d='M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 0 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 0 0-4.243-4.243L6.586 4.672z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment