From f9a7d7ecf6f1cb1798537142eca656d2fafa414b Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Thu, 20 May 2021 14:43:30 +0200
Subject: [PATCH] Handled errors in transfer (empty protocols list)

---
 .../ia2/vospace/ui/client/VOSpaceClient.java  | 50 +++++++++--
 .../vospace/ui/controller/JobController.java  |  2 +-
 .../ui/controller/NodesController.java        |  2 +-
 .../ui/controller/UploadController.java       |  2 +-
 .../vospace/ui/client/VOSpaceClientTest.java  | 83 ++++++++++++++++++-
 .../ui/controller/JobControllerTest.java      |  7 +-
 .../transfer-response-no-protocols.xml        |  5 ++
 .../test/resources/transfer-response-ok.xml   |  8 ++
 8 files changed, 144 insertions(+), 15 deletions(-)
 create mode 100644 vospace-ui-backend/src/test/resources/transfer-response-no-protocols.xml
 create mode 100644 vospace-ui-backend/src/test/resources/transfer-response-ok.xml

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 daf6d1c..34e53b4 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
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import it.inaf.ia2.aa.data.User;
 import it.inaf.ia2.vospace.ui.VOSpaceUiApplication;
 import it.inaf.ia2.vospace.ui.data.Job;
+import it.inaf.ia2.vospace.ui.exception.BadRequestException;
 import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
 import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
 import java.io.IOException;
@@ -21,11 +22,16 @@ import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Scanner;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ForkJoinPool;
+import java.util.function.BiFunction;
 import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
@@ -96,7 +102,7 @@ public class VOSpaceClient {
         return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class));
     }
 
-    public List<Protocol> getFileServiceEndpoints(Transfer transfer) {
+    public String getFileServiceEndpoint(Transfer transfer) {
 
         HttpRequest request = getRequest("/synctrans")
                 .header("Accept", useJson ? "application/json" : "text/xml")
@@ -104,7 +110,32 @@ public class VOSpaceClient {
                 .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer)))
                 .build();
 
-        return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class)).getProtocols();
+        List<Protocol> protocols = new ArrayList<>();
+
+        HttpResponse<InputStream> redirectResponse = call(request, BodyHandlers.ofInputStream(), 200, (res, previousRes) -> {
+            Transfer transferRes = unmarshal(res, Transfer.class);
+            protocols.addAll(transferRes.getProtocols());
+            // IMPORTANT: HTTP call for checking the error status (in case of empty protocols list) must be
+            // performed outside this lambda otherwise the exception "IllegalStateException: No thread-bound request found"
+            // will be thrown. For this reason the previous response data is returned here and checked later.
+            return previousRes;
+        });
+
+        if (protocols.isEmpty()) {
+            redirectResponse.headers().firstValue("Location").ifPresent(url -> {
+                Pattern pattern = Pattern.compile(".*/transfers/(.+)/results/transferDetails");
+                Matcher matcher = pattern.matcher(url);
+                if (matcher.matches()) {
+                    String jobId = matcher.group(1);
+                    String errorDetail = getErrorDetail(jobId);
+                    if (!errorDetail.isBlank()) {
+                        throw new BadRequestException(errorDetail);
+                    }
+                }
+            });
+            throw new BadRequestException("Protocol negotiation failed");
+        }
+        return protocols.get(0).getEndpoint();
     }
 
     public Node createNode(Node node) {
@@ -168,18 +199,27 @@ public class VOSpaceClient {
 
         return call(request, BodyHandlers.ofString(), 200, res -> res);
     }
-    
+
     private <T, U> U call(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, int expectedStatusCode, Function<T, U> responseHandler) {
+        return call(request, responseBodyHandler, expectedStatusCode, (res, prev) -> {
+            return responseHandler.apply(res);
+        });
+    }
+
+    private <T, U> U call(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, int expectedStatusCode, BiFunction<T, HttpResponse<T>, U> responseHandler) {
         try {
             return httpClient.sendAsync(request, responseBodyHandler)
                     .thenApply(response -> {
                         if (response.statusCode() == expectedStatusCode) {
-                            return response.body();
+                            return response;
                         }
                         logServerError(request, response);
                         throw new VOSpaceException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode());
                     })
-                    .thenApplyAsync(response -> responseHandler.apply(response), jaxbExecutor)
+                    .thenApplyAsync(response -> {
+                        HttpResponse<T> prev = response.previousResponse().orElse(null);
+                        return responseHandler.apply(response.body(), prev);
+                    }, jaxbExecutor)
                     .join();
         } catch (CompletionException ex) {
             if (ex.getCause() != null) {
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
index 514d7a1..4afb13a 100644
--- 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
@@ -158,7 +158,7 @@ public class JobController extends BaseController {
         protocol.setUri("ivo://ivoa.net/vospace/core#httpput");
         transfer.getProtocols().add(protocol);
 
-        return client.getFileServiceEndpoints(transfer).get(0).getEndpoint();
+        return client.getFileServiceEndpoint(transfer);
     }
 
     private void upload(String endpoint, String content) {
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
index 143030f..05992e8 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
@@ -80,7 +80,7 @@ public class NodesController extends BaseController {
         protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
         transfer.getProtocols().add(protocol);
 
-        String url = client.getFileServiceEndpoints(transfer).get(0).getEndpoint();
+        String url = client.getFileServiceEndpoint(transfer);
         HttpHeaders headers = new HttpHeaders();
         headers.set("Location", url);
         return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java
index 4307eae..370018f 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java
@@ -96,6 +96,6 @@ public class UploadController extends BaseController {
         protocol.setUri("ivo://ivoa.net/vospace/core#httpput");
         transfer.getProtocols().add(protocol);
 
-        return client.getFileServiceEndpoints(transfer).get(0).getEndpoint();
+        return client.getFileServiceEndpoint(transfer);
     }
 }
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 e9558bb..efb8197 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
@@ -5,17 +5,23 @@
  */
 package it.inaf.ia2.vospace.ui.client;
 
+import it.inaf.ia2.vospace.ui.exception.BadRequestException;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UncheckedIOException;
 import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
 import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import javax.servlet.http.HttpServletRequest;
 import net.ivoa.xml.vospace.v2.ContainerNode;
+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.assertThrows;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -94,7 +100,7 @@ public class VOSpaceClientTest {
 
         voSpaceClient.createNode(newNode);
     }
- 
+
     @Test
     public void testGetErrorDetail() {
 
@@ -104,6 +110,81 @@ public class VOSpaceClientTest {
         assertEquals("error message", voSpaceClient.getErrorDetail("123"));
     }
 
+    @Test
+    public void testGetFileServiceEndpointSuccess() throws Exception {
+
+        Transfer transfer = new Transfer();
+        transfer.setDirection("pushToVoSpace");
+        transfer.setTarget("vos://ia2.inaf.it!vospace/mynode");
+
+        Protocol protocol = new Protocol();
+        protocol.setUri("ivo://ivoa.net/vospace/core#httpput");
+        transfer.getProtocols().add(protocol);
+
+        CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("transfer-response-ok.xml"));
+        when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response);
+
+        assertEquals("http://storage1.example.com/trans/mynode", voSpaceClient.getFileServiceEndpoint(transfer));
+    }
+
+    @Test
+    public void testGetFileServiceEndpointError() {
+
+        Transfer transfer = new Transfer();
+        transfer.setDirection("pushToVoSpace");
+        transfer.setTarget("vos://ia2.inaf.it!vospace/mynode");
+
+        Protocol protocol = new Protocol();
+        protocol.setUri("ivo://ivoa.net/vospace/core#httpput");
+        transfer.getProtocols().add(protocol);
+
+        HttpResponse<InputStream> redirectResponse = mock(HttpResponse.class);
+        HttpHeaders headers = mock(HttpHeaders.class);
+        when(headers.firstValue("Location")).thenReturn(Optional.of("/vospace/transfers/1234/results/transferDetails"));
+        when(redirectResponse.headers()).thenReturn(headers);
+
+        HttpResponse<InputStream> mockedStreamResponse = getMockedStreamResponse(200, getResourceFileContent("transfer-response-no-protocols.xml"));
+        when(mockedStreamResponse.previousResponse()).thenReturn(Optional.of(redirectResponse));
+
+        CompletableFuture response1 = CompletableFuture.completedFuture(mockedStreamResponse);
+        CompletableFuture response2 = getMockedStringResponseFuture(200, "error message");
+        when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response1).thenReturn(response2);
+
+        BadRequestException ex = assertThrows(BadRequestException.class, () -> {
+            voSpaceClient.getFileServiceEndpoint(transfer);
+        });
+        assertEquals("error message", ex.getMessage());
+    }
+
+    @Test
+    public void testGetFileServiceEndpointErrorWithoutMessage() {
+
+        Transfer transfer = new Transfer();
+        transfer.setDirection("pushToVoSpace");
+        transfer.setTarget("vos://ia2.inaf.it!vospace/mynode");
+
+        Protocol protocol = new Protocol();
+        protocol.setUri("ivo://ivoa.net/vospace/core#httpput");
+        transfer.getProtocols().add(protocol);
+
+        HttpResponse<InputStream> redirectResponse = mock(HttpResponse.class);
+        HttpHeaders headers = mock(HttpHeaders.class);
+        when(headers.firstValue("Location")).thenReturn(Optional.of("/vospace/transfers/1234/results/transferDetails"));
+        when(redirectResponse.headers()).thenReturn(headers);
+
+        HttpResponse<InputStream> mockedStreamResponse = getMockedStreamResponse(200, getResourceFileContent("transfer-response-no-protocols.xml"));
+        when(mockedStreamResponse.previousResponse()).thenReturn(Optional.of(redirectResponse));
+
+        CompletableFuture response1 = CompletableFuture.completedFuture(mockedStreamResponse);
+        CompletableFuture response2 = getMockedStringResponseFuture(200, "");
+        when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response1).thenReturn(response2);
+
+        BadRequestException ex = assertThrows(BadRequestException.class, () -> {
+            voSpaceClient.getFileServiceEndpoint(transfer);
+        });
+        assertEquals("Protocol negotiation failed", ex.getMessage());
+    }
+
     protected static String getResourceFileContent(String fileName) {
         try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) {
             return new String(in.readAllBytes(), StandardCharsets.UTF_8);
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java
index 7c14afe..72302f3 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java
@@ -6,13 +6,11 @@
 package it.inaf.ia2.vospace.ui.controller;
 
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
-import java.util.Collections;
 import java.util.function.Consumer;
 import net.ivoa.xml.uws.v1.ErrorSummary;
 import net.ivoa.xml.uws.v1.ErrorType;
 import net.ivoa.xml.uws.v1.ExecutionPhase;
 import net.ivoa.xml.uws.v1.JobSummary;
-import net.ivoa.xml.vospace.v2.Protocol;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import org.junit.jupiter.api.Test;
@@ -68,10 +66,7 @@ public class JobControllerTest {
             return transfer.getTarget().startsWith("vos://example.com!vospace/path/to/.tmp-");
         }))).thenReturn(job);
 
-        Protocol protocol = new Protocol();
-        protocol.setEndpoint("http://file-service/path/to/file");
-
-        when(client.getFileServiceEndpoints(any())).thenReturn(Collections.singletonList(protocol));
+        when(client.getFileServiceEndpoint(any())).thenReturn("http://file-service/path/to/file");
 
         mockMvc.perform(post("/recall")
                 .contentType(MediaType.APPLICATION_JSON)
diff --git a/vospace-ui-backend/src/test/resources/transfer-response-no-protocols.xml b/vospace-ui-backend/src/test/resources/transfer-response-no-protocols.xml
new file mode 100644
index 0000000..0ae228e
--- /dev/null
+++ b/vospace-ui-backend/src/test/resources/transfer-response-no-protocols.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<vos:transfer xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" version="2.1">
+    <vos:target>vos://example.com!vospace/mynode</vos:target>
+    <vos:direction>pushToVoSpace</vos:direction>
+</vos:transfer>
\ No newline at end of file
diff --git a/vospace-ui-backend/src/test/resources/transfer-response-ok.xml b/vospace-ui-backend/src/test/resources/transfer-response-ok.xml
new file mode 100644
index 0000000..091880a
--- /dev/null
+++ b/vospace-ui-backend/src/test/resources/transfer-response-ok.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<vos:transfer xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" version="2.1">
+    <vos:target>vos://example.com!vospace/mynode</vos:target>
+    <vos:direction>pushToVoSpace</vos:direction>
+    <vos:protocol uri="ivo://ivoa.net/vospace/core#httpput">
+        <vos:endpoint>http://storage1.example.com/trans/mynode</vos:endpoint>
+    </vos:protocol>
+</vos:transfer>
\ No newline at end of file
-- 
GitLab