Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +16 −0 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.xml.bind.JAXB; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.uws.v1.Jobs; import net.ivoa.xml.vospace.v2.Node; Loading Loading @@ -189,6 +190,21 @@ public class VOSpaceClient { }); } public ExecutionPhase getJobPhase(String jobId) { HttpRequest request = getRequest("/transfers/" + jobId + "/phase") .GET() .build(); return call(request, BodyHandlers.ofInputStream(), 200, res -> { try { return ExecutionPhase.valueOf(new String(res.readAllBytes())); } catch (IOException ex) { throw new UncheckedIOException(ex); } }); } public String getErrorDetail(String jobId) { HttpRequest request = getRequest("/transfers/" + jobId + "/error") Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +29 −2 Original line number Diff line number Diff line Loading @@ -7,7 +7,9 @@ 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.Job; import it.inaf.ia2.vospace.ui.data.ListNodeData; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator; import it.inaf.ia2.vospace.ui.service.MoveNodeModalHtmlGenerator; import it.inaf.oats.vospace.datamodel.NodeUtils; Loading @@ -18,6 +20,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Node; Loading Loading @@ -46,6 +49,9 @@ public class NodesController extends BaseController { @Value("${vospace-authority}") private String authority; @Value("${maxPollingAttempts:10}") private int maxPollingAttempts; @Autowired private VOSpaceClient client; Loading Loading @@ -154,7 +160,7 @@ public class NodesController extends BaseController { } @PostMapping(value = "/move") public void moveNode(@RequestBody Map<String, Object> params) { public ResponseEntity<Job> moveNode(@RequestBody Map<String, Object> params) { String target = getRequiredParam(params, "target"); String direction = getRequiredParam(params, "direction"); Loading @@ -165,7 +171,28 @@ public class NodesController extends BaseController { JobSummary job = client.startTransferJob(transfer); // TODO: polling // Try to perform polling until completion. If it takes too much time // sends the execution phase to the UI and let it handles the polling. ExecutionPhase phase; int i = 0; do { phase = client.getJobPhase(job.getJobId()); if (phase == ExecutionPhase.COMPLETED) { break; } else if (phase == ExecutionPhase.ERROR) { String errorDetail = client.getErrorDetail(job.getJobId()); throw new VOSpaceException("MoveNode operation failed: " + errorDetail); } try { Thread.sleep(1000); } catch (InterruptedException ex) { } i++; } while (i < maxPollingAttempts); job.setPhase(phase); return ResponseEntity.ok(new Job(job)); } protected String getPath(String prefix) { Loading vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java +10 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Optional; import java.util.concurrent.CompletableFuture; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; Loading Loading @@ -186,6 +187,15 @@ public class VOSpaceClientTest { assertEquals("Protocol negotiation failed", ex.getMessage()); } @Test public void testGetJobPhase() { CompletableFuture response = getMockedStreamResponseFuture(200, "COMPLETED"); when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); assertEquals(ExecutionPhase.COMPLETED, voSpaceClient.getJobPhase("job_id")); } protected static String getResourceFileContent(String fileName) { try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); Loading vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java +72 −0 Original line number Diff line number Diff line Loading @@ -7,12 +7,19 @@ 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.data.Job; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import java.util.Arrays; import java.util.List; import java.util.Map; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.DataNode; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; Loading @@ -25,7 +32,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; Loading @@ -33,6 +42,7 @@ import org.springframework.web.util.NestedServletException; @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = {"vospace-authority=example.com!vospace", "maxPollingAttempts=2"}) public class NodesControllerTest { private static final ObjectMapper MAPPER = new ObjectMapper(); Loading Loading @@ -139,4 +149,66 @@ public class NodesControllerTest { verify(client, times(1)).getNode(eq("/a/b/c")); } @Test public void testMoveNodeSuccess() throws Exception { when(client.getJobPhase("job_id")) .thenReturn(ExecutionPhase.EXECUTING) .thenReturn(ExecutionPhase.COMPLETED); String response = testMoveNode() .andExpect(status().isOk()) .andReturn().getResponse() .getContentAsString(); Job job = MAPPER.readValue(response, Job.class); assertEquals(ExecutionPhase.COMPLETED, job.getPhase()); } @Test public void testMoveNodeExecuting() throws Exception { when(client.getJobPhase("job_id")) .thenReturn(ExecutionPhase.EXECUTING); String response = testMoveNode() .andExpect(status().isOk()) .andReturn().getResponse() .getContentAsString(); Job job = MAPPER.readValue(response, Job.class); assertEquals(ExecutionPhase.EXECUTING, job.getPhase()); } @Test public void testMoveNodeError() throws Exception { when(client.getJobPhase("job_id")) .thenReturn(ExecutionPhase.ERROR); when(client.getErrorDetail("job_id")).thenReturn("move_error"); try { testMoveNode(); fail("Exception was expected"); } catch (Exception ex) { assertTrue(ex.getCause() instanceof VOSpaceException); assertTrue(ex.getCause().getMessage().contains("move_error")); } } private ResultActions testMoveNode() throws Exception { JobSummary job = new JobSummary(); job.setJobId("job_id"); when(client.startTransferJob(any())).thenReturn(job); Map<String, String> params = Map.of("target", "/path/to/target", "direction", "/path/to/direction"); return mockMvc.perform(post("/move") .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(params))); } } vospace-ui-frontend/src/store.js +9 −4 Original line number Diff line number Diff line Loading @@ -207,11 +207,16 @@ export default new Vuex.Store({ dispatch('setPath', state.path); }); }, moveNode({ state, dispatch }, data) { moveNode({ state, commit, dispatch }, data) { client.moveNode(data) .then(() => { .then(job => { if (job.phase === 'COMPLETED') { // Reload current node dispatch('setPath', state.path); } else { main.showInfo('Move operation is taking some time and will be handled in background'); commit('addJob', job); } }); }, openNodeInMoveModal({ state, commit }, path) { Loading Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +16 −0 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.xml.bind.JAXB; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.uws.v1.Jobs; import net.ivoa.xml.vospace.v2.Node; Loading Loading @@ -189,6 +190,21 @@ public class VOSpaceClient { }); } public ExecutionPhase getJobPhase(String jobId) { HttpRequest request = getRequest("/transfers/" + jobId + "/phase") .GET() .build(); return call(request, BodyHandlers.ofInputStream(), 200, res -> { try { return ExecutionPhase.valueOf(new String(res.readAllBytes())); } catch (IOException ex) { throw new UncheckedIOException(ex); } }); } public String getErrorDetail(String jobId) { HttpRequest request = getRequest("/transfers/" + jobId + "/error") Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +29 −2 Original line number Diff line number Diff line Loading @@ -7,7 +7,9 @@ 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.Job; import it.inaf.ia2.vospace.ui.data.ListNodeData; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator; import it.inaf.ia2.vospace.ui.service.MoveNodeModalHtmlGenerator; import it.inaf.oats.vospace.datamodel.NodeUtils; Loading @@ -18,6 +20,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Node; Loading Loading @@ -46,6 +49,9 @@ public class NodesController extends BaseController { @Value("${vospace-authority}") private String authority; @Value("${maxPollingAttempts:10}") private int maxPollingAttempts; @Autowired private VOSpaceClient client; Loading Loading @@ -154,7 +160,7 @@ public class NodesController extends BaseController { } @PostMapping(value = "/move") public void moveNode(@RequestBody Map<String, Object> params) { public ResponseEntity<Job> moveNode(@RequestBody Map<String, Object> params) { String target = getRequiredParam(params, "target"); String direction = getRequiredParam(params, "direction"); Loading @@ -165,7 +171,28 @@ public class NodesController extends BaseController { JobSummary job = client.startTransferJob(transfer); // TODO: polling // Try to perform polling until completion. If it takes too much time // sends the execution phase to the UI and let it handles the polling. ExecutionPhase phase; int i = 0; do { phase = client.getJobPhase(job.getJobId()); if (phase == ExecutionPhase.COMPLETED) { break; } else if (phase == ExecutionPhase.ERROR) { String errorDetail = client.getErrorDetail(job.getJobId()); throw new VOSpaceException("MoveNode operation failed: " + errorDetail); } try { Thread.sleep(1000); } catch (InterruptedException ex) { } i++; } while (i < maxPollingAttempts); job.setPhase(phase); return ResponseEntity.ok(new Job(job)); } protected String getPath(String prefix) { Loading
vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java +10 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Optional; import java.util.concurrent.CompletableFuture; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; Loading Loading @@ -186,6 +187,15 @@ public class VOSpaceClientTest { assertEquals("Protocol negotiation failed", ex.getMessage()); } @Test public void testGetJobPhase() { CompletableFuture response = getMockedStreamResponseFuture(200, "COMPLETED"); when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); assertEquals(ExecutionPhase.COMPLETED, voSpaceClient.getJobPhase("job_id")); } protected static String getResourceFileContent(String fileName) { try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); Loading
vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java +72 −0 Original line number Diff line number Diff line Loading @@ -7,12 +7,19 @@ 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.data.Job; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import java.util.Arrays; import java.util.List; import java.util.Map; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.DataNode; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; Loading @@ -25,7 +32,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; Loading @@ -33,6 +42,7 @@ import org.springframework.web.util.NestedServletException; @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = {"vospace-authority=example.com!vospace", "maxPollingAttempts=2"}) public class NodesControllerTest { private static final ObjectMapper MAPPER = new ObjectMapper(); Loading Loading @@ -139,4 +149,66 @@ public class NodesControllerTest { verify(client, times(1)).getNode(eq("/a/b/c")); } @Test public void testMoveNodeSuccess() throws Exception { when(client.getJobPhase("job_id")) .thenReturn(ExecutionPhase.EXECUTING) .thenReturn(ExecutionPhase.COMPLETED); String response = testMoveNode() .andExpect(status().isOk()) .andReturn().getResponse() .getContentAsString(); Job job = MAPPER.readValue(response, Job.class); assertEquals(ExecutionPhase.COMPLETED, job.getPhase()); } @Test public void testMoveNodeExecuting() throws Exception { when(client.getJobPhase("job_id")) .thenReturn(ExecutionPhase.EXECUTING); String response = testMoveNode() .andExpect(status().isOk()) .andReturn().getResponse() .getContentAsString(); Job job = MAPPER.readValue(response, Job.class); assertEquals(ExecutionPhase.EXECUTING, job.getPhase()); } @Test public void testMoveNodeError() throws Exception { when(client.getJobPhase("job_id")) .thenReturn(ExecutionPhase.ERROR); when(client.getErrorDetail("job_id")).thenReturn("move_error"); try { testMoveNode(); fail("Exception was expected"); } catch (Exception ex) { assertTrue(ex.getCause() instanceof VOSpaceException); assertTrue(ex.getCause().getMessage().contains("move_error")); } } private ResultActions testMoveNode() throws Exception { JobSummary job = new JobSummary(); job.setJobId("job_id"); when(client.startTransferJob(any())).thenReturn(job); Map<String, String> params = Map.of("target", "/path/to/target", "direction", "/path/to/direction"); return mockMvc.perform(post("/move") .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(params))); } }
vospace-ui-frontend/src/store.js +9 −4 Original line number Diff line number Diff line Loading @@ -207,11 +207,16 @@ export default new Vuex.Store({ dispatch('setPath', state.path); }); }, moveNode({ state, dispatch }, data) { moveNode({ state, commit, dispatch }, data) { client.moveNode(data) .then(() => { .then(job => { if (job.phase === 'COMPLETED') { // Reload current node dispatch('setPath', state.path); } else { main.showInfo('Move operation is taking some time and will be handled in background'); commit('addJob', job); } }); }, openNodeInMoveModal({ state, commit }, path) { Loading