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

Node listing implementation

parent d71c5542
No related branches found
No related tags found
No related merge requests found
Showing with 188 additions and 58 deletions
...@@ -14,6 +14,7 @@ import java.net.http.HttpResponse.BodyHandlers; ...@@ -14,6 +14,7 @@ import java.net.http.HttpResponse.BodyHandlers;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.function.Function; import java.util.function.Function;
import javax.xml.bind.JAXB;
import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Node;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
...@@ -25,6 +26,9 @@ public class VOSpaceClient { ...@@ -25,6 +26,9 @@ public class VOSpaceClient {
private static final Logger LOG = LoggerFactory.getLogger(VOSpaceClient.class); private static final Logger LOG = LoggerFactory.getLogger(VOSpaceClient.class);
@Value("${use-json}")
private boolean useJson;
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();
private final HttpClient httpClient; private final HttpClient httpClient;
...@@ -45,8 +49,8 @@ public class VOSpaceClient { ...@@ -45,8 +49,8 @@ public class VOSpaceClient {
public Node getNode(String path) { public Node getNode(String path) {
HttpRequest request = getRequest("/nodes/" + path) HttpRequest request = getRequest("/nodes" + path)
.header("Accept", "application/json") .header("Accept", useJson ? "application/json" : "text/xml")
.build(); .build();
return call(request, BodyHandlers.ofInputStream(), 200, res -> parseJson(res, Node.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> parseJson(res, Node.class));
...@@ -84,9 +88,13 @@ public class VOSpaceClient { ...@@ -84,9 +88,13 @@ public class VOSpaceClient {
return HttpRequest.newBuilder(URI.create(baseUrl + path)); 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 { try {
if (useJson) {
return MAPPER.readValue(in, type); return MAPPER.readValue(in, type);
} else {
return JAXB.unmarshal(in, type);
}
} catch (IOException ex) { } catch (IOException ex) {
LOG.error("Invalid JSON for class {}", type.getCanonicalName()); LOG.error("Invalid JSON for class {}", type.getCanonicalName());
throw new UncheckedIOException(ex); throw new UncheckedIOException(ex);
......
package it.inaf.ia2.vospace.ui.controller; package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.vospace.ui.service.NodesService; 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.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
...@@ -18,16 +18,22 @@ public class NodesController { ...@@ -18,16 +18,22 @@ public class NodesController {
* This is the only API endpoint that returns HTML code instead of JSON. The * 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 * reason is that JavaScript frameworks are not very efficient in handling
* very long lists and tables, so this part of the code is generated * 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) @GetMapping(value = {"/nodes", "/nodes/**"}, produces = MediaType.TEXT_PLAIN_VALUE)
public void listNodes(@PathVariable(value = "path", required = false) String path, HttpServletResponse response) throws Exception { public String listNodes(HttpServletRequest request) throws Exception {
if (path == null || path.isBlank()) { String requestURL = request.getRequestURL().toString();
path = "/"; 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}") @GetMapping(value = "/download/{path}")
......
package it.inaf.ia2.vospace.ui.service; package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import java.io.OutputStream; import java.io.IOException;
import java.io.PrintWriter; import java.io.StringWriter;
import java.io.UncheckedIOException;
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.Node;
import org.slf4j.Logger; import org.slf4j.Logger;
...@@ -22,20 +23,24 @@ public class NodesService { ...@@ -22,20 +23,24 @@ public class NodesService {
@Value("${vospace-authority}") @Value("${vospace-authority}")
private String authority; private String authority;
public void generateNodesHtml(String path, OutputStream out) { public String generateNodesHtml(String path) {
Node node = client.getNode(path); Node node = client.getNode(path);
try ( PrintWriter pw = new PrintWriter(out)) { try ( StringWriter sw = new StringWriter()) {
if (node instanceof ContainerNode) { if (node instanceof ContainerNode) {
ContainerNode folder = (ContainerNode) node; ContainerNode folder = (ContainerNode) node;
pw.println("<tbody id=\"nodes\">"); sw.write("<tbody id=\"nodes\">");
for (Node child : folder.getNodes().getNode()) { 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 { ...@@ -67,7 +72,7 @@ public class NodesService {
private String getLink(NodeInfo nodeInfo) { private String getLink(NodeInfo nodeInfo) {
if (isDownloadable(nodeInfo)) { if (isDownloadable(nodeInfo)) {
if ("vos:ContainerNode".equals(nodeInfo.getType())) { 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 { } else {
return "<a href=\"download" + nodeInfo.getPath() + "\" target=\"blank_\">" + nodeInfo.getName() + "</a>"; return "<a href=\"download" + nodeInfo.getPath() + "\" target=\"blank_\">" + nodeInfo.getName() + "</a>";
} }
......
...@@ -2,6 +2,7 @@ server.port=8085 ...@@ -2,6 +2,7 @@ server.port=8085
vospace-backend-url=http://localhost:8083/vospace vospace-backend-url=http://localhost:8083/vospace
vospace-authority=example.com!vospace vospace-authority=example.com!vospace
use-json=true
# For development only: # For development only:
spring.profiles.active=dev spring.profiles.active=dev
......
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"));
}
}
...@@ -53,7 +53,7 @@ function loading(value) { ...@@ -53,7 +53,7 @@ function loading(value) {
export default { export default {
getNode(path) { getNode(path) {
let url = BASE_API_URL + 'nodes' + path; let url = BASE_API_URL + 'nodes/' + path;
return apiRequest({ return apiRequest({
method: 'GET', method: 'GET',
url: url, url: url,
......
<template> <template>
<div class="container"> <div class="container">
<b-breadcrumb :items="breadcrumbs"></b-breadcrumb>
<b-card> <b-card>
<table class="table b-table table-striped table-hover"> <table class="table b-table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th id="checkboxes"> <th id="checkboxes">
<a href="#" @click.stop.prevent="selectAll"><BIconCheckSquare /></a> <a href="#" @click.stop.prevent="selectAll">
<BIconCheckSquare />
</a>
&nbsp; &nbsp;
<a href="#" @click.stop.prevent="deSelectAll"><BIconSquare /></a> <a href="#" @click.stop.prevent="deSelectAll">
<BIconSquare />
</a>
</th> </th>
<th>Name</th> <th>Name</th>
<th>Size</th> <th>Size</th>
...@@ -22,20 +27,41 @@ ...@@ -22,20 +27,41 @@
</template> </template>
<script> <script>
import client from 'api-client';
import { BIconCheckSquare, BIconSquare } from 'bootstrap-vue' import { BIconCheckSquare, BIconSquare } from 'bootstrap-vue'
export default { export default {
components: { components: {
BIconCheckSquare, BIconSquare BIconCheckSquare,
BIconSquare
}, },
mounted() { computed: {
client.getNode(this.$store.state.path) breadcrumbs() {
.then(res => { let items = [{
document.getElementById('nodes').outerHTML = res; text: 'ROOT',
href: '#/nodes/'
}];
if (this.$store.state.path) {
let pathSplit = this.$store.state.path.split('/');
for (let i = 0; i < pathSplit.length; i++) {
items.push({
text: pathSplit[i],
href: '#/nodes/' + pathSplit.slice(0, i + 1).join('/')
}); });
}
}
return items;
}
},
created() {
this.loadNode();
},
watch: {
'$route.params.path': 'loadNode'
}, },
methods: { methods: {
loadNode() {
this.$store.dispatch('setPath', this.$route.params.path);
},
selectAll() { selectAll() {
this.selectInputs(true); this.selectInputs(true);
}, },
...@@ -56,6 +82,10 @@ export default { ...@@ -56,6 +82,10 @@ export default {
text-align: left; text-align: left;
} }
#nodes>tr>td:nth-child(2) {
white-space: nowrap;
}
th#checkboxes { th#checkboxes {
/* Credits: https://stackoverflow.com/a/43615091/771431 */ /* Credits: https://stackoverflow.com/a/43615091/771431 */
width: 0.1%; width: 0.1%;
......
...@@ -5,6 +5,10 @@ import Main from './components/Main.vue'; ...@@ -5,6 +5,10 @@ import Main from './components/Main.vue';
export default new VueRouter({ export default new VueRouter({
routes: [{ routes: [{
path: '/', path: '/',
redirect: '/nodes/'
}, {
name: 'nodes',
path: '/nodes/:path(.*)',
component: Main component: Main
}] }]
}) })
...@@ -2,18 +2,35 @@ ...@@ -2,18 +2,35 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import client from 'api-client';
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
path: '/', //breadcrumbs: [],
path: '',
loading: true loading: true
}, },
mutations: { mutations: {
setLoading(state, loading) { setLoading(state, loading) {
state.loading = loading; state.loading = loading;
},
setPath(state, value) {
if (!value) {
value = '';
}
state.path = value;
//state.breadcrumbs = value.split('/')
} }
}, },
actions: {} actions: {
setPath({ state, commit }, path) {
commit('setPath', path);
client.getNode(state.path)
.then(res => {
document.getElementById('nodes').outerHTML = res
});
}
}
}); });
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment