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

Used Jsoup for generating server-side HTML fragments

parent 84d7c5fc
No related branches found
No related tags found
No related merge requests found
...@@ -34,6 +34,11 @@ ...@@ -34,6 +34,11 @@
<artifactId>auth-lib</artifactId> <artifactId>auth-lib</artifactId>
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId> <artifactId>spring-boot-devtools</artifactId>
......
...@@ -3,7 +3,7 @@ package it.inaf.ia2.vospace.ui.controller; ...@@ -3,7 +3,7 @@ package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.aa.data.User; import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.ListNodeData; import it.inaf.ia2.vospace.ui.data.ListNodeData;
import it.inaf.ia2.vospace.ui.service.NodesService; import it.inaf.ia2.vospace.ui.service.NodesHtmlGenerator;
import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.datamodel.NodeUtils;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -12,6 +12,7 @@ import java.util.concurrent.CompletableFuture; ...@@ -12,6 +12,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.Property;
import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.Transfer;
...@@ -36,9 +37,6 @@ public class NodesController extends BaseController { ...@@ -36,9 +37,6 @@ public class NodesController extends BaseController {
@Value("${vospace-authority}") @Value("${vospace-authority}")
private String authority; private String authority;
@Autowired
private NodesService nodesService;
@Autowired @Autowired
private VOSpaceClient client; private VOSpaceClient client;
...@@ -51,7 +49,16 @@ public class NodesController extends BaseController { ...@@ -51,7 +49,16 @@ public class NodesController extends BaseController {
String path = getPath("/nodes/"); String path = getPath("/nodes/");
LOG.debug("listNodes called for path {}", path); LOG.debug("listNodes called for path {}", path);
return ResponseEntity.ok(nodesService.generateNodesHtml(path, principal)); ListNodeData listNodeData = new ListNodeData();
Node node = client.getNode(path);
listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups()));
NodesHtmlGenerator htmlGenerator = new NodesHtmlGenerator(node, principal, authority);
listNodeData.setHtmlTable(htmlGenerator.generateNodes());
return ResponseEntity.ok(listNodeData);
} }
@GetMapping(value = "/download/**") @GetMapping(value = "/download/**")
......
package it.inaf.ia2.vospace.ui.service; package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeProperties;
import it.inaf.oats.vospace.datamodel.NodeUtils;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.DataNode;
...@@ -29,8 +31,10 @@ public class NodeInfo { ...@@ -29,8 +31,10 @@ public class NodeInfo {
private final boolean sticky; private final boolean sticky;
private final boolean busy; private final boolean busy;
private final boolean listOfFiles; private final boolean listOfFiles;
private final boolean writable;
private final boolean deletable;
public NodeInfo(Node node, String authority) { public NodeInfo(Node node, User user, String authority) {
this.authority = authority; this.authority = authority;
this.path = getPath(node); this.path = getPath(node);
this.name = path.substring(path.lastIndexOf("/") + 1); this.name = path.substring(path.lastIndexOf("/") + 1);
...@@ -44,6 +48,8 @@ public class NodeInfo { ...@@ -44,6 +48,8 @@ public class NodeInfo {
this.sticky = isSticky(node); this.sticky = isSticky(node);
this.busy = isBusy(node); this.busy = isBusy(node);
this.listOfFiles = isListOfFiles(node); this.listOfFiles = isListOfFiles(node);
this.writable = NodeUtils.checkIfWritable(node, user.getName(), user.getGroups());
this.deletable = writable && !sticky;
} }
private String getPath(Node node) { private String getPath(Node node) {
...@@ -189,4 +195,12 @@ public class NodeInfo { ...@@ -189,4 +195,12 @@ public class NodeInfo {
public boolean isListOfFiles() { public boolean isListOfFiles() {
return listOfFiles; return listOfFiles;
} }
public boolean isWritable() {
return writable;
}
public boolean isDeletable() {
return deletable;
}
} }
package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.aa.data.User;
import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Node;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
public class NodesHtmlGenerator {
private final Node parentNode;
private final User user;
private final String authority;
private Element tbody;
public NodesHtmlGenerator(Node node, User user, String authority) {
this.parentNode = node;
this.user = user;
this.authority = authority;
}
public String generateNodes() {
Document html = Jsoup.parse("<html></html>");
tbody = html.body().appendElement("tbody");
tbody.attr("id", "nodes");
if (parentNode instanceof ContainerNode) {
ContainerNode folder = (ContainerNode) parentNode;
for (Node child : folder.getNodes()) {
addChild(child);
}
}
return tbody.toString();
}
private void addChild(Node child) {
NodeInfo nodeInfo = new NodeInfo(child, user, authority);
if (nodeInfo.isListOfFiles()) {
// hidden file
return;
}
Element row = tbody.appendElement("tr");
addSelectionCell(nodeInfo, row);
addLinkCell(nodeInfo, row);
addSizeCell(nodeInfo, row);
addGroupReadCell(nodeInfo, row);
addGroupWriteCell(nodeInfo, row);
addActionsCell(nodeInfo, row);
}
private void addSelectionCell(NodeInfo nodeInfo, Element row) {
Element cell = row.appendElement("td");
Element input = cell.appendElement("input");
input.attr("type", "checkbox");
input.attr("data-node", nodeInfo.getPath());
if (nodeInfo.isAsyncTrans()) {
input.addClass("async");
} else if (nodeInfo.isDeletable()) {
input.addClass("deletable");
}
}
private void addLinkCell(NodeInfo nodeInfo, Element row) {
Element cell = row.appendElement("td");
addNodeIcon(nodeInfo, cell);
addLink(nodeInfo, cell);
}
private void addSizeCell(NodeInfo nodeInfo, Element row) {
Element cell = row.appendElement("td");
cell.text(nodeInfo.getSize());
}
private void addGroupReadCell(NodeInfo nodeInfo, Element row) {
Element cell = row.appendElement("td");
fillGroupCell(cell, nodeInfo.getGroupRead());
}
private void addGroupWriteCell(NodeInfo nodeInfo, Element row) {
Element cell = row.appendElement("td");
fillGroupCell(cell, nodeInfo.getGroupWrite());
}
private void fillGroupCell(Element cell, String groups) {
String[] values = groups.split(" ");
List<String> personGroups = new ArrayList<>();
List<String> peopleGroups = new ArrayList<>();
for (String value : values) {
if (value.startsWith("people.")) {
personGroups.add(value.substring("people.".length()).replace("\\.", "."));
} else {
peopleGroups.add(value);
}
}
if (!personGroups.isEmpty()) {
Element personIcon = cell.appendElement("span");
personIcon.attr("class", "icon person-icon");
cell.appendText(String.join(" ", personGroups));
cell.append("&nbsp;");
}
if (!peopleGroups.isEmpty()) {
Element personIcon = cell.appendElement("span");
personIcon.attr("class", "icon people-icon");
cell.appendText(String.join(" ", peopleGroups));
}
}
private void addActionsCell(NodeInfo nodeInfo, Element row) {
Element cell = row.appendElement("td");
if (nodeInfo.isWritable()) {
Element shareIcon = cell.appendElement("span");
shareIcon.attr("class", "icon share-icon pointer");
shareIcon.attr("onclick", "shareNode(" + makeJsArg(nodeInfo.getPath())
+ "," + makeJsArg(nodeInfo.getGroupRead())
+ "," + makeJsArg(nodeInfo.getGroupWrite()) + ")");
}
if (nodeInfo.isDeletable()) {
cell.append("&nbsp;");
Element deleteIcon = cell.appendElement("span");
deleteIcon.attr("class", "icon trash-icon pointer");
deleteIcon.attr("onclick", "deleteNode(" + makeJsArg(nodeInfo.getPath()) + ")");
}
}
private String makeJsArg(String arg) {
return "'" + arg.replace("\\", "\\\\").replace("'", "\\'") + "'";
}
private void addNodeIcon(NodeInfo nodeInfo, Element cell) {
Element iconContainer = cell;
if (nodeInfo.isBusy()) {
Element loadingWrapper = cell.appendElement("span");
iconContainer = loadingWrapper;
loadingWrapper.addClass("node-busy");
Element spinner = loadingWrapper.appendElement("span");
spinner.attr("role", "status");
spinner.addClass("spinner-border");
Element srEl = spinner.appendElement("span");
srEl.addClass("sr-only");
srEl.text("Loading...");
}
Element icon = cell.appendElement("span");
icon.addClass("icon");
if (nodeInfo.isFolder()) {
if (nodeInfo.isAsyncTrans()) {
icon.addClass("folder-x-icon");
} else {
icon.addClass("folder-icon");
}
} else {
if (nodeInfo.isAsyncTrans()) {
icon.addClass("file-x-icon");
} else {
icon.addClass("file-icon");
}
}
iconContainer.append("&nbsp;");
}
private void addLink(NodeInfo nodeInfo, Element cell) {
if (isDownloadable(nodeInfo, user)) {
Element link = cell.appendElement("a");
String href;
if (nodeInfo.isFolder()) {
href = "#/nodes" + urlEncodePath(nodeInfo.getPath());
} else {
href = "download" + urlEncodePath(nodeInfo.getPath());
link.attr("target", "blank_");
}
link.attr("href", href);
link.text(nodeInfo.getName());
} else {
cell.text(nodeInfo.getName());
}
}
private boolean isDownloadable(NodeInfo nodeInfo, User user) {
if (nodeInfo.isFile()) {
if (nodeInfo.isAsyncTrans() || nodeInfo.isBusy()) {
return false;
}
}
if (nodeInfo.isPublic()) {
return true;
}
if (nodeInfo.getCreator().equals(user.getName())) {
return true;
}
if (user.getGroups() != null && !user.getGroups().isEmpty() && !nodeInfo.getGroupRead().isEmpty()) {
List<String> groupRead = Arrays.asList(nodeInfo.getGroupRead().split(" "));
for (String group : groupRead) {
if (user.getGroups().contains(group)) {
return true;
}
}
}
return false;
}
}
package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.ListNodeData;
import it.inaf.oats.vospace.datamodel.NodeUtils;
import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.List;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Node;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class NodesService {
private static final Logger LOG = LoggerFactory.getLogger(NodesService.class);
@Autowired
private VOSpaceClient client;
@Value("${vospace-authority}")
private String authority;
public ListNodeData generateNodesHtml(String path, User user) {
ListNodeData listNodeData = new ListNodeData();
Node node = client.getNode(path);
listNodeData.setWritable(NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()));
try (StringWriter sw = new StringWriter()) {
if (node instanceof ContainerNode) {
ContainerNode folder = (ContainerNode) node;
sw.write("<tbody id=\"nodes\">");
for (Node child : folder.getNodes()) {
sw.write(getNodeHtml(child, user));
}
sw.write("</tbody>");
}
listNodeData.setHtmlTable(sw.toString());
return listNodeData;
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private String getNodeHtml(Node node, User user) {
NodeInfo nodeInfo = new NodeInfo(node, authority);
if (nodeInfo.isListOfFiles()) {
// hidden file
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>";
html += "<td>" + nodeInfo.getSize() + "</td>";
html += "<td>" + nodeInfo.getGroupRead() + "</td>";
html += "<td>" + nodeInfo.getGroupWrite() + "</td>";
html += "<td>";
if (deletable) {
html += "<span class=\"icon trash-icon pointer\" onclick=\"deleteNode('" + nodeInfo.getPath() + "')\"></span>";
}
html += "</td>";
html += "</tr>";
return html;
}
private String getIcon(NodeInfo nodeInfo) {
String html = "";
if (nodeInfo.isBusy()) {
html += "<span class=\"node-busy\"><span role=\"status\" class=\"spinner-border\"><span class=\"sr-only\">Loading...</span></span>";
}
html += "<span class=\"icon ";
if (nodeInfo.isFolder()) {
html += "folder";
} else {
html += "file";
}
if (nodeInfo.isAsyncTrans()) {
html += "-x";
}
html += "-icon\"></span>";
if (nodeInfo.isBusy()) {
html += "</span>";
}
html += "&nbsp;";
return html;
}
private String getLink(NodeInfo nodeInfo, User user) {
if (isDownloadable(nodeInfo, user)) {
if (nodeInfo.isFolder()) {
return "<a href=\"#/nodes" + urlEncodePath(nodeInfo.getPath()) + "\">" + nodeInfo.getName() + "</a>";
} else {
return "<a href=\"download" + urlEncodePath(nodeInfo.getPath()) + "\" target=\"blank_\">" + nodeInfo.getName() + "</a>";
}
}
return nodeInfo.getName();
}
private boolean isDownloadable(NodeInfo nodeInfo, User user) {
if (nodeInfo.isFile()) {
if (nodeInfo.isAsyncTrans() || nodeInfo.isBusy()) {
return false;
}
}
if (nodeInfo.isPublic()) {
return true;
}
if (nodeInfo.getCreator().equals(user.getName())) {
return true;
}
if (user.getGroups() != null && !user.getGroups().isEmpty() && !nodeInfo.getGroupRead().isEmpty()) {
List<String> groupRead = Arrays.asList(nodeInfo.getGroupRead().split(" "));
for (String group : groupRead) {
if (user.getGroups().contains(group)) {
return true;
}
}
}
return false;
}
}
...@@ -2,17 +2,17 @@ package it.inaf.ia2.vospace.ui.controller; ...@@ -2,17 +2,17 @@ package it.inaf.ia2.vospace.ui.controller;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.service.NodesService;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import net.ivoa.xml.vospace.v2.DataNode;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import org.mockito.Mockito;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
...@@ -30,9 +30,6 @@ public class NodesControllerTest { ...@@ -30,9 +30,6 @@ public class NodesControllerTest {
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();
@MockBean
private NodesService nodesService;
@MockBean @MockBean
private VOSpaceClient client; private VOSpaceClient client;
...@@ -42,28 +39,34 @@ public class NodesControllerTest { ...@@ -42,28 +39,34 @@ public class NodesControllerTest {
@Test @Test
public void testListNodesEmpty() throws Exception { public void testListNodesEmpty() throws Exception {
when(client.getNode(any())).thenReturn(new DataNode());
mockMvc.perform(get("/nodes")) mockMvc.perform(get("/nodes"))
.andExpect(status().isOk()); .andExpect(status().isOk());
verify(nodesService).generateNodesHtml(eq("/"), any()); verify(client, times(1)).getNode(eq("/"));
} }
@Test @Test
public void testListNodesRoot() throws Exception { public void testListNodesRoot() throws Exception {
when(client.getNode(any())).thenReturn(new DataNode());
mockMvc.perform(get("/nodes/")) mockMvc.perform(get("/nodes/"))
.andExpect(status().isOk()); .andExpect(status().isOk());
verify(nodesService).generateNodesHtml(eq("/"), any()); verify(client, times(1)).getNode(eq("/"));
} }
@Test @Test
public void testListNodesComplexPath() throws Exception { public void testListNodesComplexPath() throws Exception {
when(client.getNode(any())).thenReturn(new DataNode());
mockMvc.perform(get("/nodes/a/b/c")) mockMvc.perform(get("/nodes/a/b/c"))
.andExpect(status().isOk()); .andExpect(status().isOk());
verify(nodesService).generateNodesHtml(eq("/a/b/c"), any()); verify(client, times(1)).getNode(eq("/a/b/c"));
} }
@Test @Test
......
package it.inaf.ia2.vospace.ui.service; package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.aa.data.User;
import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.Property;
...@@ -48,7 +49,7 @@ public class NodeInfoTest { ...@@ -48,7 +49,7 @@ public class NodeInfoTest {
private void testNodeLength(long bytes, String expectedText) { private void testNodeLength(long bytes, String expectedText) {
DataNode node = getDataNode(); DataNode node = getDataNode();
setLength(node, bytes); setLength(node, bytes);
NodeInfo nodeInfo = new NodeInfo(node, AUTHORITY); NodeInfo nodeInfo = new NodeInfo(node, new User(), AUTHORITY);
assertEquals(expectedText, nodeInfo.getSize()); assertEquals(expectedText, nodeInfo.getSize());
} }
......
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 net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Property;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public class NodesHtmlGeneratorTest {
@Test
public void testGenerateNodes() {
ContainerNode parent = new ContainerNode();
parent.setUri("vos://example.com!vospace/mynode");
setGroups(parent, "group1", "group1");
DataNode file1 = new DataNode();
file1.setUri("vos://example.com!vospace/mynode/file1");
setGroups(file1, "group1", "group2 people.name\\.surname");
parent.getNodes().add(file1);
User user = new User();
user.setUserId("user_id");
user.setGroups(Arrays.asList("group1", "group2"));
NodesHtmlGenerator generator = new NodesHtmlGenerator(parent, user, "example.com!vospace");
String html = generator.generateNodes();
System.out.println(html);
assertTrue(html.startsWith("<tbody id=\"nodes\">"));
assertTrue(html.contains("<input type=\"checkbox\" data-node=\"/mynode/file1\" class=\"deletable\">"));
}
private void setGroups(Node node, String groupRead, String groupWrite) {
Property groupReadProperty = new Property();
groupReadProperty.setUri(NodeProperties.GROUP_READ_URI);
groupReadProperty.setValue(groupRead);
Property groupWriteProperty = new Property();
groupWriteProperty.setUri(NodeProperties.GROUP_WRITE_URI);
groupWriteProperty.setValue(groupWrite);
node.getProperties().add(groupReadProperty);
node.getProperties().add(groupWriteProperty);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment