diff --git a/vospace-ui-backend/pom.xml b/vospace-ui-backend/pom.xml index 44cdc591f0e31dd859de6850efb092e22fcf32f0..fcbf7bea58370387060d482d07702c0d3a875aa1 100644 --- a/vospace-ui-backend/pom.xml +++ b/vospace-ui-backend/pom.xml @@ -34,6 +34,11 @@ <artifactId>auth-lib</artifactId> <version>2.0.0-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.jsoup</groupId> + <artifactId>jsoup</artifactId> + <version>1.13.1</version> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java index 2490d3155134deb9dc0bcb72ce4b13d2538e9b79..61dc48225a214ec766d2461cad58495f7459f47e 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java @@ -3,7 +3,7 @@ package it.inaf.ia2.vospace.ui.controller; 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.ia2.vospace.ui.service.NodesService; +import it.inaf.ia2.vospace.ui.service.NodesHtmlGenerator; import it.inaf.oats.vospace.datamodel.NodeUtils; import java.util.List; import java.util.Map; @@ -12,6 +12,7 @@ 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.Node; import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; @@ -32,13 +33,10 @@ import org.springframework.web.bind.annotation.RestController; public class NodesController extends BaseController { private static final Logger LOG = LoggerFactory.getLogger(NodesController.class); - + @Value("${vospace-authority}") private String authority; - @Autowired - private NodesService nodesService; - @Autowired private VOSpaceClient client; @@ -51,7 +49,16 @@ public class NodesController extends BaseController { String path = getPath("/nodes/"); 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/**") @@ -82,7 +89,7 @@ public class NodesController extends BaseController { parentPath = "/" + parentPath; } String name = getRequiredParam(params, "name"); - + LOG.debug("newFolder called for path {}/{}", parentPath, name); ContainerNode node = new ContainerNode(); @@ -121,7 +128,7 @@ public class NodesController extends BaseController { // 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); diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java index 77e86146bea92000cefd2600ff981b994b74b800..6025e00d3f141cb01a08f2948d032ee11d547075 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java @@ -1,7 +1,9 @@ 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.oats.vospace.datamodel.NodeProperties; +import it.inaf.oats.vospace.datamodel.NodeUtils; import java.util.List; import java.util.Optional; import net.ivoa.xml.vospace.v2.DataNode; @@ -29,8 +31,10 @@ public class NodeInfo { private final boolean sticky; private final boolean busy; 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.path = getPath(node); this.name = path.substring(path.lastIndexOf("/") + 1); @@ -44,6 +48,8 @@ public class NodeInfo { this.sticky = isSticky(node); this.busy = isBusy(node); this.listOfFiles = isListOfFiles(node); + this.writable = NodeUtils.checkIfWritable(node, user.getName(), user.getGroups()); + this.deletable = writable && !sticky; } private String getPath(Node node) { @@ -58,7 +64,7 @@ public class NodeInfo { return uri.substring(prefix.length()); } - + private String getCreator(Node node) { return getProperty(node, NodeProperties.CREATOR_URI).orElse(""); } @@ -82,7 +88,7 @@ public class NodeInfo { private boolean isSticky(Node node) { return getProperty(node, NodeProperties.STICKY_URN).map(value -> "true".equals(value)).orElse(false); } - + private boolean isBusy(Node node) { return node instanceof DataNode && ((DataNode) node).isBusy(); } @@ -189,4 +195,12 @@ public class NodeInfo { public boolean isListOfFiles() { return listOfFiles; } + + public boolean isWritable() { + return writable; + } + + public boolean isDeletable() { + return deletable; + } } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..e45c778e6675acbf48a6d2315edf693ccfc5f1de --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java @@ -0,0 +1,223 @@ +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(" "); + } + 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(" "); + 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(" "); + } + + 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; + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java deleted file mode 100644 index 0934c8d16138e14cb386a8b40d16db4764277cca..0000000000000000000000000000000000000000 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java +++ /dev/null @@ -1,149 +0,0 @@ -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 += " "; - 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; - } -} diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java index 39e3863a578840147ef792f196675ee67457c515..110a932d989b4a2cfef615141fa79e4a94590535 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java @@ -2,17 +2,17 @@ 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 net.ivoa.xml.vospace.v2.DataNode; 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 static org.mockito.Mockito.when; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -30,9 +30,6 @@ public class NodesControllerTest { private static final ObjectMapper MAPPER = new ObjectMapper(); - @MockBean - private NodesService nodesService; - @MockBean private VOSpaceClient client; @@ -42,28 +39,34 @@ public class NodesControllerTest { @Test public void testListNodesEmpty() throws Exception { + when(client.getNode(any())).thenReturn(new DataNode()); + mockMvc.perform(get("/nodes")) .andExpect(status().isOk()); - verify(nodesService).generateNodesHtml(eq("/"), any()); + verify(client, times(1)).getNode(eq("/")); } @Test public void testListNodesRoot() throws Exception { + when(client.getNode(any())).thenReturn(new DataNode()); + mockMvc.perform(get("/nodes/")) .andExpect(status().isOk()); - verify(nodesService).generateNodesHtml(eq("/"), any()); + verify(client, times(1)).getNode(eq("/")); } @Test public void testListNodesComplexPath() throws Exception { + when(client.getNode(any())).thenReturn(new DataNode()); + mockMvc.perform(get("/nodes/a/b/c")) .andExpect(status().isOk()); - verify(nodesService).generateNodesHtml(eq("/a/b/c"), any()); + verify(client, times(1)).getNode(eq("/a/b/c")); } @Test diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodeInfoTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodeInfoTest.java index f113953ce11b382eb568c499d8f097d0276fb1db..102bdc98b2a4920e5028e8f1b30671b2f6f83038 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodeInfoTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodeInfoTest.java @@ -1,5 +1,6 @@ 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.Node; import net.ivoa.xml.vospace.v2.Property; @@ -48,7 +49,7 @@ public class NodeInfoTest { private void testNodeLength(long bytes, String expectedText) { DataNode node = getDataNode(); setLength(node, bytes); - NodeInfo nodeInfo = new NodeInfo(node, AUTHORITY); + NodeInfo nodeInfo = new NodeInfo(node, new User(), AUTHORITY); assertEquals(expectedText, nodeInfo.getSize()); } diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a462ac7708c173a03b2f5dd9a6bd47565ddc434b --- /dev/null +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java @@ -0,0 +1,53 @@ +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); + } +}