diff --git a/src/main/java/it/inaf/oats/vospace/TransferController.java b/src/main/java/it/inaf/oats/vospace/TransferController.java index 5169f98621c79be4333e43066c7388128013036b..330e1dcb4068cb3bcfba69dce5b3216a38f65e34 100644 --- a/src/main/java/it/inaf/oats/vospace/TransferController.java +++ b/src/main/java/it/inaf/oats/vospace/TransferController.java @@ -1,7 +1,6 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; -import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.persistence.JobDAO; import java.util.Optional; import java.util.UUID; @@ -10,7 +9,6 @@ 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.Transfer; -import net.ivoa.xml.vospace.v2.Param; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -24,8 +22,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; -import java.util.stream.Collectors; +import net.ivoa.xml.vospace.v2.Protocol; @RestController public class TransferController { @@ -70,6 +70,41 @@ public class TransferController { return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER); } + @GetMapping("/synctrans") + public ResponseEntity<?> syncTransferUrlParamsMode(@RequestParam("TARGET") String target, + @RequestParam("DIRECTION") String direction, @RequestParam("PROTOCOL") String protocolUris, User principal) { + + Transfer transfer = new Transfer(); + transfer.setTarget(target); + transfer.setDirection(direction); + + // CADC client sends multiple protocol parameters and Spring join them using a comma separator. + // Some values seems to be duplicated, so a set is used to avoid the duplication + for (String protocolUri : new HashSet<>(Arrays.asList(protocolUris.split(",")))) { + Protocol protocol = new Protocol(); + protocol.setUri(protocolUri); + transfer.getProtocols().add(protocol); + } + + JobSummary jobSummary = newJobSummary(transfer, principal); + jobService.createSyncJobResult(jobSummary); + + if (jobSummary.getErrorSummary() != null) { + // TODO: decide how to hanlde HTTP error codes + // If an error occurs with the synchronous convenience mode where the preferred endpoint + // is immediately returned as a redirect, the error information is returned directly in + // the response body with the associated HTTP status code. + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(jobSummary.getErrorSummary().getMessage()); + } + + // Behaves as if REQUEST=redirect was set, for compatibility with CADC client + String endpoint = transfer.getProtocols().get(0).getEndpoint(); + HttpHeaders headers = new HttpHeaders(); + headers.set("Location", endpoint); + return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER); + } + @GetMapping(value = "/transfers/{jobId}", produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity<JobSummary> getJob(@PathVariable("jobId") String jobId) { return jobDAO.getJob(jobId).map(j -> ResponseEntity.ok(j)).orElse(ResponseEntity.notFound().build()); @@ -147,7 +182,7 @@ public class TransferController { private JobSummary newJobSummary(Transfer transfer, User principal) { String jobId = UUID.randomUUID().toString().replace("-", ""); - + JobSummary jobSummary = new JobSummary(); jobSummary.setJobId(jobId); jobSummary.setOwnerId(principal.getName()); diff --git a/src/main/java/it/inaf/oats/vospace/UriService.java b/src/main/java/it/inaf/oats/vospace/UriService.java index ab05851c5a4a3f90434c044a1ba2629e0fa08b0f..fdbb39f1c4c12dedcb2387f23310a5d16da39712 100644 --- a/src/main/java/it/inaf/oats/vospace/UriService.java +++ b/src/main/java/it/inaf/oats/vospace/UriService.java @@ -8,7 +8,7 @@ import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import it.inaf.oats.vospace.exception.InternalFaultException; -import it.inaf.oats.vospace.exception.InvalidURIException; +import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeNotFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.exception.ProtocolNotSupportedException; @@ -22,6 +22,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.uws.v1.ResultReference; @@ -67,28 +68,55 @@ public class UriService { job.setResults(results); // Moved phase setting to caller method for ERROR management - // job.setPhase(ExecutionPhase.COMPLETED); } + /** + * Sets the endpoint value for all valid protocols (protocol negotiation). + */ public void setSyncTransferEndpoints(JobSummary job) { Transfer transfer = getTransfer(job); - Protocol protocol = transfer.getProtocols().get(0); + if (transfer.getProtocols().isEmpty()) { + // At least one protocol is expected from client + throw new InvalidArgumentException("Transfer contains no protocols"); + } + + JobService.JobType jobType = JobType.valueOf(transfer.getDirection()); + + List<String> validProtocolUris = new ArrayList<>(); + switch (jobType) { + case pullFromVoSpace: + validProtocolUris.add("ivo://ivoa.net/vospace/core#httpget"); + break; + case pushToVoSpace: + validProtocolUris.add("ivo://ivoa.net/vospace/core#httpput"); + break; + } + + List<Protocol> validProtocols + = transfer.getProtocols().stream() + .filter(protocol -> validProtocolUris.contains(protocol.getUri())) + .collect(Collectors.toList()); - if (!"ivo://ivoa.net/vospace/core#httpget".equals(protocol.getUri()) - && !"ivo://ivoa.net/vospace/core#httpput".equals(protocol.getUri())) { - // throw new IllegalStateException("Unsupported protocol " + protocol.getUri()); + if (validProtocols.isEmpty()) { + Protocol protocol = transfer.getProtocols().get(0); throw new ProtocolNotSupportedException(protocol.getUri()); } - protocol.setEndpoint(getEndpoint(job, transfer)); + + String endpoint = getEndpoint(job, transfer); + validProtocols.stream().forEach(p -> p.setEndpoint(endpoint)); + + // Returns modified transfer containing only valid protocols + transfer.getProtocols().clear(); + transfer.getProtocols().addAll(validProtocols); } private Node getEndpointNode(String relativePath, JobService.JobType jobType, User user) { Optional<Node> optNode = nodeDao.listNode(relativePath); - if (optNode.isPresent()) { + if (optNode.isPresent()) { return optNode.get(); } else { switch (jobType) { @@ -98,11 +126,10 @@ public class UriService { case pullToVoSpace: DataNode newNode = new DataNode(); newNode.setUri(relativePath); - return createNodeController.createNode(newNode, user); + return createNodeController.createNode(newNode, user); default: throw new InternalFaultException("No supported job direction specified"); } - } } @@ -203,13 +230,13 @@ public class UriService { List<Object> jobPayload = job.getJobInfo().getAny(); if (jobPayload.isEmpty()) { - throw new IllegalStateException("Empty job payload for job " + job.getJobId()); + throw new InternalFaultException("Empty job payload for job " + job.getJobId()); } if (jobPayload.size() > 1) { - throw new IllegalStateException("Multiple objects in job payload not supported"); + throw new InternalFaultException("Multiple objects in job payload not supported"); } if (!(jobPayload.get(0) instanceof Transfer)) { - throw new IllegalStateException(jobPayload.get(0).getClass().getCanonicalName() + throw new InternalFaultException(jobPayload.get(0).getClass().getCanonicalName() + " not supported as job payload. Job id: " + job.getJobId()); } diff --git a/src/main/java/it/inaf/oats/vospace/exception/InvalidArgumentException.java b/src/main/java/it/inaf/oats/vospace/exception/InvalidArgumentException.java new file mode 100644 index 0000000000000000000000000000000000000000..d24af645b56c8f1fb4680a18eea85f20027d6d65 --- /dev/null +++ b/src/main/java/it/inaf/oats/vospace/exception/InvalidArgumentException.java @@ -0,0 +1,13 @@ +package it.inaf.oats.vospace.exception; + +import net.ivoa.xml.uws.v1.ErrorSummaryFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.BAD_REQUEST) +public class InvalidArgumentException extends VoSpaceErrorSummarizableException { + + public InvalidArgumentException(String message) { + super(message, ErrorSummaryFactory.VOSpaceFault.NODE_NOT_FOUND); + } +} diff --git a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java index 4a47a4783912e6606bea7b7dbeb96b25b94196dd..e59b314915acde93caff690554f3177d30d281d1 100644 --- a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java +++ b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java @@ -142,7 +142,7 @@ public class TransferControllerTest { testPullToVoSpace("/portalnode", getResourceFileContent("pullToVoSpace-portal.xml")); verify(nodeDao, times(1)).setNodeLocation(eq("/portalnode"), eq(2), eq("lbcr.20130512.060722.fits.gz")); - + verify(jobDao, times(2)).updateJob(argThat(j -> { assertTrue(j.getResults().get(0).getHref().startsWith("http://archive.lbto.org")); assertEquals(ExecutionPhase.COMPLETED, j.getPhase()); @@ -226,17 +226,17 @@ public class TransferControllerTest { property.setUri("ivo://ivoa.net/vospace/core#publicread"); property.setValue("true"); node.getProperties().add(property); - + Property ownerProp = new Property(); ownerProp.setUri(NodeProperties.CREATOR_URI); ownerProp.setValue("user1"); node.getProperties().add(ownerProp); - + Property groupProp = new Property(); groupProp.setUri(NodeProperties.GROUP_WRITE_URI); groupProp.setValue("group1"); node.getProperties().add(groupProp); - + return node; } @@ -280,6 +280,22 @@ public class TransferControllerTest { .andReturn().getResponse().getContentAsString(); } + @Test + public void testSyncTransferUrlParamsMode() throws Exception { + + Node node = mockPublicDataNode(); + when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(node)); + + mockMvc.perform(get("/synctrans") + .header("Authorization", "Bearer user1_token") + .param("TARGET", "vos://example.com!vospace/mynode") + .param("DIRECTION", "pullFromVoSpace") + // testing duplicated protocol (CADC client) + .param("PROTOCOL", "ivo://ivoa.net/vospace/core#httpget") + .param("PROTOCOL", "ivo://ivoa.net/vospace/core#httpget")) + .andExpect(status().is3xxRedirection()); + } + private Jobs getFakeJobs() { Jobs jobs = new Jobs(); jobs.setVersion("1.1"); diff --git a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java index cef14ed89336bb45457dcac622d0921cbed9e0c3..9fbe034100bd5e073cccc335011d0ef7a79e6b92 100644 --- a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java +++ b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java @@ -3,6 +3,8 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.ServletRapClient; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.datamodel.NodeProperties; +import it.inaf.oats.vospace.exception.InvalidArgumentException; +import it.inaf.oats.vospace.exception.ProtocolNotSupportedException; import it.inaf.oats.vospace.persistence.LocationDAO; import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.model.Location; @@ -14,8 +16,10 @@ import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Property; +import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; @@ -44,10 +48,10 @@ public class UriServiceTest { @MockBean private LocationDAO locationDAO; - + @MockBean private ServletRapClient rapClient; - + @MockBean private CreateNodeController createNodeController; @@ -66,10 +70,13 @@ public class UriServiceTest { @Bean @Primary public HttpServletRequest servletRequest() { - return mock(HttpServletRequest.class); + HttpServletRequest request = mock(HttpServletRequest.class); + User user = new User().setUserId("anonymous"); + when(request.getUserPrincipal()).thenReturn(user); + return request; } } - + @BeforeEach public void init() { Location location = new Location(); @@ -102,7 +109,7 @@ public class UriServiceTest { creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); node.getProperties().add(creator); - + Property readgroup = new Property(); readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1"); @@ -128,40 +135,39 @@ public class UriServiceTest { assertEquals("http://file-service/mydata1?jobId=job-id&token=<new-token>", job.getResults().get(0).getHref()); } - + @Test public void pushToNonexistentNode() { - + ContainerNode node = new ContainerNode(); node.setUri("vos://example.com!vospace/mydata1"); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); node.getProperties().add(creator); - + Property readgroup = new Property(); readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1"); node.getProperties().add(readgroup); - + DataNode dnode = new DataNode(); dnode.setUri("vos://example.com!vospace/mydata1/mydata2"); dnode.getProperties().add(creator); dnode.getProperties().add(readgroup); - + Property writegroup = new Property(); writegroup.setUri(NodeProperties.GROUP_WRITE_URI); writegroup.setValue("group1"); dnode.getProperties().add(writegroup); - + when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); when(nodeDAO.listNode(eq("/mydata1/mydata2"))).thenReturn(Optional.empty()); - - + User user = mock(User.class); when(user.getAccessToken()).thenReturn("<token>"); when(user.getName()).thenReturn("user1"); - + when(servletRequest.getUserPrincipal()).thenReturn(user); when(rapClient.exchangeToken(argThat(req -> { @@ -172,14 +178,14 @@ public class UriServiceTest { JobSummary job = getPushToVoSpaceJob(); Transfer tr = uriService.getTransfer(job); - + when(createNodeController.createNode(any(), eq(user))).thenReturn(dnode); - + uriService.setTransferJobResult(job, tr); - + verify(createNodeController, times(1)).createNode(any(), eq(user)); - - assertEquals("http://file-service/mydata1/mydata2?jobId=job-id2&token=<new-token>", job.getResults().get(0).getHref()); + + assertEquals("http://file-service/mydata1/mydata2?jobId=job-id2&token=<new-token>", job.getResults().get(0).getHref()); } @Test @@ -197,7 +203,131 @@ public class UriServiceTest { verify(nodeDAO).setNodeLocation(eq("/test/f1/lbtfile.fits"), eq(5), eq("lbtfile.fits")); } - + + @Test + public void testSetSyncTransferEndpointsPullFromVoSpace() { + + mockPublicNode(); + + Transfer transfer = new Transfer(); + transfer.setTarget("vos://example.com!vospace/mydata1"); + transfer.setDirection("pullFromVoSpace"); + + Protocol protocol1 = new Protocol(); + protocol1.setUri("ivo://ivoa.net/vospace/core#httpget"); + transfer.getProtocols().add(protocol1); + Protocol protocol2 = new Protocol(); + + protocol2.setUri("invalid_protocol"); + transfer.getProtocols().add(protocol2); + + JobSummary job = new JobSummary(); + JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); + jobInfo.getAny().add(transfer); + job.setJobInfo(jobInfo); + + assertEquals(2, transfer.getProtocols().size()); + + uriService.setSyncTransferEndpoints(job); + + // invalid protocol is removed + assertEquals(1, transfer.getProtocols().size()); + assertEquals("ivo://ivoa.net/vospace/core#httpget", transfer.getProtocols().get(0).getUri()); + } + + @Test + public void testSetSyncTransferEndpointsPushToVoSpace() { + + Node node = mockPublicNode(); + + Property creator = new Property(); + creator.setUri(NodeProperties.CREATOR_URI); + creator.setValue("user1"); + + node.getProperties().add(creator); + User user = mock(User.class); + when(user.getName()).thenReturn("user1"); + when(servletRequest.getUserPrincipal()).thenReturn(user); + + Transfer transfer = new Transfer(); + transfer.setTarget("vos://example.com!vospace/mydata1"); + transfer.setDirection("pushToVoSpace"); + + Protocol protocol1 = new Protocol(); + protocol1.setUri("ivo://ivoa.net/vospace/core#httpput"); + transfer.getProtocols().add(protocol1); + Protocol protocol2 = new Protocol(); + + protocol2.setUri("invalid_protocol"); + transfer.getProtocols().add(protocol2); + + JobSummary job = new JobSummary(); + JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); + jobInfo.getAny().add(transfer); + job.setJobInfo(jobInfo); + + assertEquals(2, transfer.getProtocols().size()); + + uriService.setSyncTransferEndpoints(job); + + // invalid protocol is removed + assertEquals(1, transfer.getProtocols().size()); + assertEquals("ivo://ivoa.net/vospace/core#httpput", transfer.getProtocols().get(0).getUri()); + } + + @Test + public void testSetSyncTransferEndpointsUnsupportedProtocol() { + + Transfer transfer = new Transfer(); + transfer.setTarget("vos://example.com!vospace/mydata1"); + transfer.setDirection("pullFromVoSpace"); + + Protocol protocol = new Protocol(); + protocol.setUri("invalid_protocol"); + transfer.getProtocols().add(protocol); + + JobSummary job = new JobSummary(); + JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); + jobInfo.getAny().add(transfer); + job.setJobInfo(jobInfo); + + try { + uriService.setSyncTransferEndpoints(job); + fail("Expected ProtocolNotSupportedException"); + } catch (ProtocolNotSupportedException ex) { + } + } + + @Test + public void testSetSyncTransferEndpointsNoProtocols() { + + Transfer transfer = new Transfer(); + transfer.setTarget("vos://example.com!vospace/mydata1"); + transfer.setDirection("pullFromVoSpace"); + + JobSummary job = new JobSummary(); + JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); + jobInfo.getAny().add(transfer); + job.setJobInfo(jobInfo); + + try { + uriService.setSyncTransferEndpoints(job); + fail("Expected InvalidArgumentException"); + } catch (InvalidArgumentException ex) { + } + } + + private Node mockPublicNode() { + DataNode node = new DataNode(); + node.setUri("vos://example.com!vospace/mydata1"); + Property publicProperty = new Property(); + publicProperty.setUri(NodeProperties.PUBLIC_READ_URI); + publicProperty.setValue(String.valueOf(true)); + node.getProperties().add(publicProperty); + when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); + return node; + } + private JobSummary getJob() { Transfer transfer = new Transfer(); @@ -214,7 +344,7 @@ public class UriServiceTest { return job; } - + private JobSummary getPushToVoSpaceJob() { Transfer transfer = new Transfer(); transfer.setTarget("vos://example.com!vospace/mydata1/mydata2"); @@ -228,6 +358,6 @@ public class UriServiceTest { job.setJobInfo(jobInfo); - return job; + return job; } }