Commit 3f746155 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Node listing implementation

parent d71c5542
Loading
Loading
Loading
Loading
+12 −4
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import java.net.http.HttpResponse.BodyHandlers;
import java.util.Scanner;
import java.util.concurrent.CompletionException;
import java.util.function.Function;
import javax.xml.bind.JAXB;
import net.ivoa.xml.vospace.v2.Node;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,6 +26,9 @@ public class VOSpaceClient {

    private static final Logger LOG = LoggerFactory.getLogger(VOSpaceClient.class);

    @Value("${use-json}")
    private boolean useJson;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    private final HttpClient httpClient;
@@ -45,8 +49,8 @@ public class VOSpaceClient {

    public Node getNode(String path) {

        HttpRequest request = getRequest("/nodes/" + path)
                .header("Accept", "application/json")
        HttpRequest request = getRequest("/nodes" + path)
                .header("Accept", useJson ? "application/json" : "text/xml")
                .build();

        return call(request, BodyHandlers.ofInputStream(), 200, res -> parseJson(res, Node.class));
@@ -84,9 +88,13 @@ public class VOSpaceClient {
        return HttpRequest.newBuilder(URI.create(baseUrl + path));
    }

    private static <T> T parseJson(InputStream in, Class<T> type) {
    private <T> T parseJson(InputStream in, Class<T> type) {
        try {
            if (useJson) {
                return MAPPER.readValue(in, type);
            } else {
                return JAXB.unmarshal(in, type);
            }
        } catch (IOException ex) {
            LOG.error("Invalid JSON for class {}", type.getCanonicalName());
            throw new UncheckedIOException(ex);
+13 −7
Original line number Diff line number Diff line
package it.inaf.ia2.vospace.ui.controller;

import it.inaf.ia2.vospace.ui.service.NodesService;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
@@ -18,16 +18,22 @@ public class NodesController {
     * This is the only API endpoint that returns HTML code instead of JSON. The
     * reason is that JavaScript frameworks are not very efficient in handling
     * very long lists and tables, so this part of the code is generated
     * server-side.
     * server-side. The content type is set to text/plain even if it is an HTML
     * fragment to avoid browser parsing issues since it is not a complete HTML
     * document.
     */
    @GetMapping(value = {"/nodes", "/nodes/{path}"}, produces = MediaType.TEXT_HTML_VALUE)
    public void listNodes(@PathVariable(value = "path", required = false) String path, HttpServletResponse response) throws Exception {
    @GetMapping(value = {"/nodes", "/nodes/**"}, produces = MediaType.TEXT_PLAIN_VALUE)
    public String listNodes(HttpServletRequest request) throws Exception {

        if (path == null || path.isBlank()) {
            path = "/";
        String requestURL = request.getRequestURL().toString();
        String[] split = requestURL.split("/nodes/");

        String path = "/";
        if (split.length == 2) {
            path += split[1];
        }

        nodesService.generateNodesHtml(path, response.getOutputStream());
        return nodesService.generateNodesHtml(path);
    }

    @GetMapping(value = "/download/{path}")
+13 −8
Original line number Diff line number Diff line
package it.inaf.ia2.vospace.ui.service;

import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Node;
import org.slf4j.Logger;
@@ -22,20 +23,24 @@ public class NodesService {
    @Value("${vospace-authority}")
    private String authority;

    public void generateNodesHtml(String path, OutputStream out) {
    public String generateNodesHtml(String path) {

        Node node = client.getNode(path);

        try ( PrintWriter pw = new PrintWriter(out)) {
        try ( StringWriter sw = new StringWriter()) {

            if (node instanceof ContainerNode) {
                ContainerNode folder = (ContainerNode) node;
                pw.println("<tbody id=\"nodes\">");
                sw.write("<tbody id=\"nodes\">");
                for (Node child : folder.getNodes().getNode()) {
                    pw.println(getNodeHtml(child));
                    sw.write(getNodeHtml(child));
                }
                pw.println("</tbody>");
                sw.write("</tbody>");
            }

            return sw.toString();
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

@@ -67,7 +72,7 @@ public class NodesService {
    private String getLink(NodeInfo nodeInfo) {
        if (isDownloadable(nodeInfo)) {
            if ("vos:ContainerNode".equals(nodeInfo.getType())) {
                return "<a href=\"#nodes" + nodeInfo.getPath() + "\">" + nodeInfo.getName() + "</a>";
                return "<a href=\"#/nodes" + nodeInfo.getPath() + "\">" + nodeInfo.getName() + "</a>";
            } else {
                return "<a href=\"download" + nodeInfo.getPath() + "\" target=\"blank_\">" + nodeInfo.getName() + "</a>";
            }
+1 −0
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ server.port=8085

vospace-backend-url=http://localhost:8083/vospace
vospace-authority=example.com!vospace
use-json=true

# For development only:
spring.profiles.active=dev
+59 −0
Original line number Diff line number Diff line
package it.inaf.ia2.vospace.ui.controller;

import it.inaf.ia2.vospace.ui.service.NodesService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.mockito.ArgumentMatchers.eq;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.verify;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@ExtendWith(MockitoExtension.class)
public class NodesControllerTest {

    @Mock
    private NodesService nodesService;

    @InjectMocks
    private NodesController controller;

    private MockMvc mockMvc;

    @BeforeEach
    public void init() {
        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    }

    @Test
    public void testListNodesEmpty() throws Exception {

        mockMvc.perform(get("/nodes"))
                .andExpect(status().isOk());

        verify(nodesService).generateNodesHtml(eq("/"));
    }

    @Test
    public void testListNodesRoot() throws Exception {

        mockMvc.perform(get("/nodes/"))
                .andExpect(status().isOk());

        verify(nodesService).generateNodesHtml(eq("/"));
    }

    @Test
    public void testListNodesComplexPath() throws Exception {

        mockMvc.perform(get("/nodes/a/b/c"))
                .andExpect(status().isOk());

        verify(nodesService).generateNodesHtml(eq("/a/b/c"));
    }
}
Loading