Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +2 −1 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ 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.VOSpaceStatusException; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.io.IOException; Loading Loading @@ -256,7 +257,7 @@ public class VOSpaceClient { return response; } logServerError(request, response); throw new VOSpaceException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode()); throw new VOSpaceStatusException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode(), response.statusCode()); }) .thenApplyAsync(response -> { HttpResponse<T> prev = response.previousResponse().orElse(null); Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java +35 −6 Original line number Diff line number Diff line Loading @@ -6,8 +6,11 @@ package it.inaf.ia2.vospace.ui.controller; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.PreUploadResult; import it.inaf.ia2.vospace.ui.data.UploadFilesData; import it.inaf.ia2.vospace.ui.exception.PermissionDeniedException; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.util.Arrays; import java.util.List; Loading @@ -18,6 +21,8 @@ import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; Loading @@ -29,6 +34,8 @@ import org.springframework.web.bind.annotation.RestController; @RestController public class UploadController extends BaseController { private static final Logger LOG = LoggerFactory.getLogger(UploadController.class); @Value("${vospace-authority}") private String authority; Loading @@ -36,17 +43,17 @@ public class UploadController extends BaseController { private VOSpaceClient client; @PostMapping(value = "/preupload", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<List<String>> prepareForUpload(@RequestBody @Valid UploadFilesData data) { public ResponseEntity<List<PreUploadResult>> prepareForUpload(@RequestBody @Valid UploadFilesData data) { if (getUser() == null) { throw new PermissionDeniedException("File upload not allowed to anonymous users"); } CompletableFuture<String>[] calls CompletableFuture<PreUploadResult>[] calls = data.getFiles().stream().map(fileName -> prepareForDownload(getParentPath(data), fileName)) .toArray(CompletableFuture[]::new); List<String> uploadUrls = CompletableFuture.allOf(calls) List<PreUploadResult> uploadUrls = CompletableFuture.allOf(calls) .thenApplyAsync(ignore -> { return Arrays.stream(calls).map(c -> c.join()).collect(Collectors.toList()); }).join(); Loading @@ -62,7 +69,7 @@ public class UploadController extends BaseController { return parentPath; } public CompletableFuture<String> prepareForDownload(String parentPath, String fileName) { public CompletableFuture<PreUploadResult> prepareForDownload(String parentPath, String fileName) { return CompletableFuture.supplyAsync(() -> { Loading @@ -75,9 +82,31 @@ public class UploadController extends BaseController { String nodeUri = "vos://" + authority + urlEncodePath(path); PreUploadResult result = new PreUploadResult(); try { createDataNode(nodeUri, getUser().getName()); } catch (Throwable t) { if (t instanceof VOSpaceStatusException && ((VOSpaceStatusException) t).getHttpStatus() == 409) { result.setError("Node already exists"); } else { LOG.error("Error while creating node metadata for " + nodeUri, t); result.setError("Unable to create node metadata"); } return result; } try { String uploadUrl = obtainUploadUrl(nodeUri); result.setUrl(uploadUrl); } catch (VOSpaceException ex) { result.setError(ex.getMessage()); } catch (Throwable t) { LOG.error("Error while obtaining upload URL for " + nodeUri, t); result.setError("Unable to obtain upload URL"); } return obtainUploadUrl(nodeUri); return result; }, Runnable::run); // Passing current thread Executor to CompletableFuture to avoid "No thread-bound request found" exception } Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/PreUploadResult.java 0 → 100644 +35 −0 Original line number Diff line number Diff line /* * This file is part of vospace-ui * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.ia2.vospace.ui.data; /** * Represents the result of the operations necessary before uploading a file * (node metadata creation and generation of pushToVoSpace upload URL). If one * of these operations fails the UI shows the error message for the specific * file involved in the failure rather than aborting the whole upload (that can * be composed by multiple files). */ public class PreUploadResult { private String url; private String error; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getError() { return error; } public void setError(String error) { this.error = error; } } vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/exception/VOSpaceStatusException.java 0 → 100644 +20 −0 Original line number Diff line number Diff line /* * This file is part of vospace-ui * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.ia2.vospace.ui.exception; public class VOSpaceStatusException extends VOSpaceException { private final int httpStatus; public VOSpaceStatusException(String message, int httpStatus) { super(message); this.httpStatus = httpStatus; } public int getHttpStatus() { return httpStatus; } } vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java +84 −2 Original line number Diff line number Diff line Loading @@ -9,11 +9,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.UploadFilesData; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import java.util.List; import static org.hamcrest.core.IsNull.nullValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import org.mockito.Mock; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; Loading Loading @@ -63,7 +67,8 @@ public class UploadControllerTest { .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0]").value("http://files/mynode/test.txt")); .andExpect(jsonPath("$[0].url").value("http://files/mynode/test.txt")) .andExpect(jsonPath("$[0].error").value(nullValue())); } @Test Loading @@ -81,7 +86,8 @@ public class UploadControllerTest { .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0]").value("http://files/test.txt")); .andExpect(jsonPath("$[0].url").value("http://files/test.txt")) .andExpect(jsonPath("$[0].error").value(nullValue())); } @Test Loading @@ -96,4 +102,80 @@ public class UploadControllerTest { .content(MAPPER.writeValueAsString(data))) .andExpect(status().isForbidden()); } @Test public void testDuplicatedNodeError() throws Exception { UploadFilesData data = new UploadFilesData(); data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); doThrow(new VOSpaceStatusException("Conflict", 409)).when(client).createNode(any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].error").value("Node already exists")) .andExpect(jsonPath("$[0].url").value(nullValue())); } @Test public void testGenericMetadataCreationError() throws Exception { UploadFilesData data = new UploadFilesData(); data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); doThrow(new VOSpaceStatusException("Server Error", 500)).when(client).createNode(any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].error").value("Unable to create node metadata")) .andExpect(jsonPath("$[0].url").value(nullValue())); } @Test public void testUploadUrlRecognizedError() throws Exception { UploadFilesData data = new UploadFilesData(); data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); doThrow(new VOSpaceException("Unable to connect")).when(client).getFileServiceEndpoint(any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].error").value("Unable to connect")) .andExpect(jsonPath("$[0].url").value(nullValue())); } @Test public void testUploadUrlFatalError() throws Exception { UploadFilesData data = new UploadFilesData(); data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); doThrow(new NullPointerException()).when(client).getFileServiceEndpoint(any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].error").value("Unable to obtain upload URL")) .andExpect(jsonPath("$[0].url").value(nullValue())); } } Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +2 −1 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ 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.VOSpaceStatusException; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.io.IOException; Loading Loading @@ -256,7 +257,7 @@ public class VOSpaceClient { return response; } logServerError(request, response); throw new VOSpaceException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode()); throw new VOSpaceStatusException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode(), response.statusCode()); }) .thenApplyAsync(response -> { HttpResponse<T> prev = response.previousResponse().orElse(null); Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java +35 −6 Original line number Diff line number Diff line Loading @@ -6,8 +6,11 @@ package it.inaf.ia2.vospace.ui.controller; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.PreUploadResult; import it.inaf.ia2.vospace.ui.data.UploadFilesData; import it.inaf.ia2.vospace.ui.exception.PermissionDeniedException; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.util.Arrays; import java.util.List; Loading @@ -18,6 +21,8 @@ import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; Loading @@ -29,6 +34,8 @@ import org.springframework.web.bind.annotation.RestController; @RestController public class UploadController extends BaseController { private static final Logger LOG = LoggerFactory.getLogger(UploadController.class); @Value("${vospace-authority}") private String authority; Loading @@ -36,17 +43,17 @@ public class UploadController extends BaseController { private VOSpaceClient client; @PostMapping(value = "/preupload", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<List<String>> prepareForUpload(@RequestBody @Valid UploadFilesData data) { public ResponseEntity<List<PreUploadResult>> prepareForUpload(@RequestBody @Valid UploadFilesData data) { if (getUser() == null) { throw new PermissionDeniedException("File upload not allowed to anonymous users"); } CompletableFuture<String>[] calls CompletableFuture<PreUploadResult>[] calls = data.getFiles().stream().map(fileName -> prepareForDownload(getParentPath(data), fileName)) .toArray(CompletableFuture[]::new); List<String> uploadUrls = CompletableFuture.allOf(calls) List<PreUploadResult> uploadUrls = CompletableFuture.allOf(calls) .thenApplyAsync(ignore -> { return Arrays.stream(calls).map(c -> c.join()).collect(Collectors.toList()); }).join(); Loading @@ -62,7 +69,7 @@ public class UploadController extends BaseController { return parentPath; } public CompletableFuture<String> prepareForDownload(String parentPath, String fileName) { public CompletableFuture<PreUploadResult> prepareForDownload(String parentPath, String fileName) { return CompletableFuture.supplyAsync(() -> { Loading @@ -75,9 +82,31 @@ public class UploadController extends BaseController { String nodeUri = "vos://" + authority + urlEncodePath(path); PreUploadResult result = new PreUploadResult(); try { createDataNode(nodeUri, getUser().getName()); } catch (Throwable t) { if (t instanceof VOSpaceStatusException && ((VOSpaceStatusException) t).getHttpStatus() == 409) { result.setError("Node already exists"); } else { LOG.error("Error while creating node metadata for " + nodeUri, t); result.setError("Unable to create node metadata"); } return result; } try { String uploadUrl = obtainUploadUrl(nodeUri); result.setUrl(uploadUrl); } catch (VOSpaceException ex) { result.setError(ex.getMessage()); } catch (Throwable t) { LOG.error("Error while obtaining upload URL for " + nodeUri, t); result.setError("Unable to obtain upload URL"); } return obtainUploadUrl(nodeUri); return result; }, Runnable::run); // Passing current thread Executor to CompletableFuture to avoid "No thread-bound request found" exception } Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/PreUploadResult.java 0 → 100644 +35 −0 Original line number Diff line number Diff line /* * This file is part of vospace-ui * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.ia2.vospace.ui.data; /** * Represents the result of the operations necessary before uploading a file * (node metadata creation and generation of pushToVoSpace upload URL). If one * of these operations fails the UI shows the error message for the specific * file involved in the failure rather than aborting the whole upload (that can * be composed by multiple files). */ public class PreUploadResult { private String url; private String error; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getError() { return error; } public void setError(String error) { this.error = error; } }
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/exception/VOSpaceStatusException.java 0 → 100644 +20 −0 Original line number Diff line number Diff line /* * This file is part of vospace-ui * Copyright (C) 2021 Istituto Nazionale di Astrofisica * SPDX-License-Identifier: GPL-3.0-or-later */ package it.inaf.ia2.vospace.ui.exception; public class VOSpaceStatusException extends VOSpaceException { private final int httpStatus; public VOSpaceStatusException(String message, int httpStatus) { super(message); this.httpStatus = httpStatus; } public int getHttpStatus() { return httpStatus; } }
vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java +84 −2 Original line number Diff line number Diff line Loading @@ -9,11 +9,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.UploadFilesData; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import java.util.List; import static org.hamcrest.core.IsNull.nullValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import org.mockito.Mock; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; Loading Loading @@ -63,7 +67,8 @@ public class UploadControllerTest { .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0]").value("http://files/mynode/test.txt")); .andExpect(jsonPath("$[0].url").value("http://files/mynode/test.txt")) .andExpect(jsonPath("$[0].error").value(nullValue())); } @Test Loading @@ -81,7 +86,8 @@ public class UploadControllerTest { .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0]").value("http://files/test.txt")); .andExpect(jsonPath("$[0].url").value("http://files/test.txt")) .andExpect(jsonPath("$[0].error").value(nullValue())); } @Test Loading @@ -96,4 +102,80 @@ public class UploadControllerTest { .content(MAPPER.writeValueAsString(data))) .andExpect(status().isForbidden()); } @Test public void testDuplicatedNodeError() throws Exception { UploadFilesData data = new UploadFilesData(); data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); doThrow(new VOSpaceStatusException("Conflict", 409)).when(client).createNode(any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].error").value("Node already exists")) .andExpect(jsonPath("$[0].url").value(nullValue())); } @Test public void testGenericMetadataCreationError() throws Exception { UploadFilesData data = new UploadFilesData(); data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); doThrow(new VOSpaceStatusException("Server Error", 500)).when(client).createNode(any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].error").value("Unable to create node metadata")) .andExpect(jsonPath("$[0].url").value(nullValue())); } @Test public void testUploadUrlRecognizedError() throws Exception { UploadFilesData data = new UploadFilesData(); data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); doThrow(new VOSpaceException("Unable to connect")).when(client).getFileServiceEndpoint(any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].error").value("Unable to connect")) .andExpect(jsonPath("$[0].url").value(nullValue())); } @Test public void testUploadUrlFatalError() throws Exception { UploadFilesData data = new UploadFilesData(); data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); doThrow(new NullPointerException()).when(client).getFileServiceEndpoint(any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) .contentType(MediaType.APPLICATION_JSON) .content(MAPPER.writeValueAsString(data))) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].error").value("Unable to obtain upload URL")) .andExpect(jsonPath("$[0].url").value(nullValue())); } }