From fb0bb97e4045adfc75f5d94a2da62eb4907c5252 Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Fri, 18 Dec 2020 17:12:16 +0100 Subject: [PATCH] Implemented basic recall from tape --- .../ia2/vospace/ui/client/VOSpaceClient.java | 35 +++++++++++- .../vospace/ui/controller/JobController.java | 57 +++++++++++++++++++ .../java/it/inaf/ia2/vospace/ui/data/Job.java | 43 ++++++++++++++ .../ia2/vospace/ui/service/NodesService.java | 2 +- .../src/main/resources/application.properties | 4 +- .../vospace/ui/client/VOSpaceClientTest.java | 3 + vospace-ui-frontend/src/api/server/index.js | 12 ++++ vospace-ui-frontend/src/components/Main.vue | 2 +- vospace-ui-frontend/src/store.js | 2 +- 9 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java index 4ca0620..9cfa265 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java @@ -6,6 +6,7 @@ import it.inaf.ia2.vospace.ui.VOSpaceException; import it.inaf.ia2.vospace.ui.VOSpaceUiApplication; import java.io.IOException; import java.io.InputStream; +import java.io.StringWriter; import java.io.UncheckedIOException; import java.net.ConnectException; import java.net.URI; @@ -20,7 +21,9 @@ import java.util.function.Function; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.xml.bind.JAXB; +import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.Node; +import net.ivoa.xml.vospace.v2.Transfer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -42,7 +45,7 @@ public class VOSpaceClient { private final ForkJoinPool jaxbExecutor; @Autowired - private HttpServletRequest servletRequest; + protected HttpServletRequest servletRequest; public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) { if (backendUrl.endsWith("/")) { @@ -65,7 +68,18 @@ public class VOSpaceClient { .header("Accept", useJson ? "application/json" : "text/xml") .build(); - return call(request, BodyHandlers.ofInputStream(), 200, res -> parseJson(res, Node.class)); + return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); + } + + public JobSummary startTransferJob(Transfer transfer) { + + HttpRequest request = getRequest("/transfers?PHASE=RUN") + .header("Accept", useJson ? "application/json" : "text/xml") + .header("Content-Type", useJson ? "application/json" : "text/xml") + .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) + .build(); + + return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class)); } private <T, U> U call(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, int expectedStatusCode, Function<T, U> responseHandler) { @@ -116,7 +130,7 @@ public class VOSpaceClient { return null; } - private <T> T parseJson(InputStream in, Class<T> type) { + private <T> T unmarshal(InputStream in, Class<T> type) { try { if (useJson) { return MAPPER.readValue(in, type); @@ -129,6 +143,21 @@ public class VOSpaceClient { } } + private String marshal(Object data) { + try { + if (useJson) { + return MAPPER.writeValueAsString(data); + } else { + try ( StringWriter sw = new StringWriter()) { + JAXB.marshal(data, sw); + return sw.toString(); + } + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + private static <T> void logServerError(HttpRequest request, HttpResponse<T> response) { if (response.body() instanceof String) { logServerErrorString(request, (HttpResponse<String>) response); diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java new file mode 100644 index 0000000..5ef1a00 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java @@ -0,0 +1,57 @@ +package it.inaf.ia2.vospace.ui.controller; + +import it.inaf.ia2.vospace.ui.client.VOSpaceClient; +import it.inaf.ia2.vospace.ui.data.Job; +import java.util.ArrayList; +import java.util.List; +import net.ivoa.xml.uws.v1.ExecutionPhase; +import net.ivoa.xml.uws.v1.JobSummary; +import net.ivoa.xml.vospace.v2.Protocol; +import net.ivoa.xml.vospace.v2.Transfer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class JobController { + + @Value("${vospace-authority}") + private String authority; + + @Autowired + private VOSpaceClient client; + + @PostMapping(value = "/recall", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity<Job> startRecallFromTapeJob(@RequestBody List<String> paths) { + + if (paths.size() != 1) { + throw new UnsupportedOperationException(); + } + + Transfer transfer = new Transfer(); + transfer.setDirection("pullToVoSpace"); + transfer.setTarget("vos://" + authority + paths.get(0)); + Protocol protocol = new Protocol(); + protocol.setUri("ia2:tape-recall"); + transfer.getProtocol().add(protocol); + + JobSummary job = client.startTransferJob(transfer); + + if (job.getPhase() == ExecutionPhase.QUEUED) { + return ResponseEntity.ok(new Job(job)); + } + // TODO: proper handling + throw new RuntimeException("Error while executing job " + job.getJobId() + ". Job phase is " + job.getPhase() + ". QUEUED expected"); + } + + @GetMapping(value = "/jobs", produces = MediaType.APPLICATION_JSON_VALUE) + public List<Job> getJobs() { + // TODO + return new ArrayList<>(); + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java new file mode 100644 index 0000000..4ea2b8c --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java @@ -0,0 +1,43 @@ +package it.inaf.ia2.vospace.ui.data; + +import net.ivoa.xml.uws.v1.ExecutionPhase; +import net.ivoa.xml.uws.v1.JobSummary; + +public class Job { + + private String id; + private ExecutionPhase phase; + private boolean read; + + public Job() { + } + + public Job(JobSummary job) { + this.id = job.getJobId(); + this.phase = job.getPhase(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public ExecutionPhase getPhase() { + return phase; + } + + public void setPhase(ExecutionPhase phase) { + this.phase = phase; + } + + public boolean isRead() { + return read; + } + + public void setRead(boolean read) { + this.read = read; + } +} 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 index 04beff8..91b0b82 100644 --- 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 @@ -85,6 +85,6 @@ public class NodesService { return true; } // TODO: check user group - return false; + return true; // temporary always true } } diff --git a/vospace-ui-backend/src/main/resources/application.properties b/vospace-ui-backend/src/main/resources/application.properties index 1b0cb7d..200742e 100644 --- a/vospace-ui-backend/src/main/resources/application.properties +++ b/vospace-ui-backend/src/main/resources/application.properties @@ -2,8 +2,10 @@ server.port=8085 vospace-backend-url=http://localhost:8083/vospace vospace-authority=example.com!vospace -use-json=true +use-json=false # For development only: spring.profiles.active=dev cors.allowed.origin=http://localhost:8080 + +logging.level.it.inaf=TRACE diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java index 2149009..107d040 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java @@ -8,6 +8,7 @@ import java.net.http.HttpClient; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; +import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.vospace.v2.ContainerNode; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.BeforeEach; @@ -40,6 +41,8 @@ public class VOSpaceClientTest { staticMock.when(HttpClient::newBuilder).thenReturn(builder); voSpaceClient = new VOSpaceClient("http://localhost/vospace"); } + + voSpaceClient.servletRequest = mock(HttpServletRequest.class); } @Test diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js index 0c5e4a9..077a7c0 100644 --- a/vospace-ui-frontend/src/api/server/index.js +++ b/vospace-ui-frontend/src/api/server/index.js @@ -72,5 +72,17 @@ export default { 'Cache-Control': 'no-cache' } }, false, false); + }, + startRecallFromTapeJob(paths) { + let url = BASE_API_URL + 'recall'; + return apiRequest({ + method: 'POST', + url: url, + withCredentials: true, + headers: { + 'Cache-Control': 'no-cache' + }, + data: paths + }); } } diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue index 99455ce..56c9563 100644 --- a/vospace-ui-frontend/src/components/Main.vue +++ b/vospace-ui-frontend/src/components/Main.vue @@ -57,7 +57,7 @@ export default { return items; }, tapeButtonEnabled() { - return this.$store.state.tapeButtonEnabled; + return this.$store.state.tapeButtonEnabled || true; // temporary always true } }, created() { diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js index af99d79..a4cb681 100644 --- a/vospace-ui-frontend/src/store.js +++ b/vospace-ui-frontend/src/store.js @@ -62,7 +62,7 @@ export default new Vuex.Store({ commit('setTapeButtonEnabled', document.querySelectorAll('#nodes input.tape:checked').length > 0); }, startRecallFromTapeJob({ commit }) { - let tapeCheckboxes = document.querySelectorAll('#nodes input.tape:checked'); + let tapeCheckboxes = document.querySelectorAll('#nodes input:checked'); // temporary: it should be input.tape let paths = []; for (let i = 0; i < tapeCheckboxes.length; i++) { paths.push(tapeCheckboxes[i].getAttribute('data-node')); -- GitLab