Commit 8eeb0a43 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Added synctrans URL parameters endpoint (GET convenience mode) and improved protocol negotiation

parent 911da09f
Loading
Loading
Loading
Loading
+39 −4
Original line number Diff line number Diff line
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());
+40 −13
Original line number Diff line number Diff line
@@ -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,21 +68,48 @@ 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,
@@ -102,7 +130,6 @@ public class UriService {
                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());
        }

+13 −0
Original line number Diff line number Diff line
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);
    }
}
+20 −4
Original line number Diff line number Diff line
@@ -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");
+152 −22
Original line number Diff line number Diff line
@@ -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;
@@ -66,7 +70,10 @@ 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;
        }
    }

@@ -157,7 +164,6 @@ public class UriServiceTest {
        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");
@@ -198,6 +204,130 @@ 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();