Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +12 −4 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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)); Loading Loading @@ -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); Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +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; Loading @@ -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}") Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java +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; Loading @@ -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); } } Loading Loading @@ -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>"; } Loading vospace-ui-backend/src/main/resources/application.properties +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java 0 → 100644 +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
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +12 −4 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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)); Loading Loading @@ -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); Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +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; Loading @@ -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}") Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java +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; Loading @@ -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); } } Loading Loading @@ -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>"; } Loading
vospace-ui-backend/src/main/resources/application.properties +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java 0 → 100644 +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")); } }