Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SessionThreadFactory.java 0 → 100644 +53 −0 Original line number Original line 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; import java.util.concurrent.ThreadFactory; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * VOSpaceClient needs to extract the access token stored into the HTTP session * and forward it to the VOSpace REST service. The client can retrieve the * session from the autowired HttpServletRequest, however this doesn't work if * the client is called from a standard new thread (for example when the client * is invoked from a CompletableFuture), because Spring Context is missing and * retrieval of the autowired HttpServletRequest proxy fails with the "No * thread-bound request found" exception. Many CompletableFuture methods accept * an Executor as parameter, so this ThreadFactory has been created in order to * store the session in these particular threads. An Executor using this * ThreadaFactory must be passed to the CompletableFuture. An alternative would * be passing the original HttpServletRequest to all VOSpaceClient methods. A * first attempt using the current executor (Runnable::run) shown that tasks * where executed sequentially, so this dedicated Executor has been set up. */ public class SessionThreadFactory implements ThreadFactory { private final HttpServletRequest request; public SessionThreadFactory(HttpServletRequest request) { this.request = request; } @Override public Thread newThread(Runnable runnable) { return new SessionThread(runnable, request); } public static class SessionThread extends Thread { private final HttpSession session; public SessionThread(Runnable runnable, HttpServletRequest request) { super(runnable); this.session = request.getSession(false); } public HttpSession getHttpSession() { return this.session; } } } vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/TokenProvider.java 0 → 100644 +32 −0 Original line number Original line 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; import it.inaf.ia2.aa.data.User; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class TokenProvider { @Autowired private HttpServletRequest request; public Optional<String> getToken() { HttpSession session = request.getSession(false); if (session != null) { User user = (User) session.getAttribute("user_data"); if (user != null) { return Optional.of(user.getAccessToken()); } } return Optional.empty(); } } vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java +12 −0 Original line number Original line Diff line number Diff line Loading @@ -12,6 +12,8 @@ import it.inaf.ia2.aa.ServletRapClient; import it.inaf.ia2.aa.UserManager; import it.inaf.ia2.aa.UserManager; import it.inaf.ia2.gms.client.GmsClient; import it.inaf.ia2.gms.client.GmsClient; import it.inaf.ia2.rap.client.ClientCredentialsRapClient; import it.inaf.ia2.rap.client.ClientCredentialsRapClient; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; Loading Loading @@ -66,6 +68,16 @@ public class VOSpaceUiApplication { return new ForkJoinPool(parallelism, threadFactory, null, false); return new ForkJoinPool(parallelism, threadFactory, null, false); } } /** * Executor to pass to CompletableFuture methods to avoid * "RejectedExecutionException: Thread limit exceeded replacing blocked * worker". */ @Bean public Executor requestsExecutor() { return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); } @Bean @Bean public RestTemplate restTemplate() { public RestTemplate restTemplate() { return new RestTemplate(); return new RestTemplate(); Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +30 −65 Original line number Original line Diff line number Diff line Loading @@ -6,7 +6,6 @@ package it.inaf.ia2.vospace.ui.client; package it.inaf.ia2.vospace.ui.client; import com.fasterxml.jackson.databind.ObjectMapper; 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.VOSpaceUiApplication; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import it.inaf.ia2.vospace.ui.exception.BadRequestException; Loading Loading @@ -35,8 +34,6 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.xml.bind.JAXB; import javax.xml.bind.JAXB; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.uws.v1.JobSummary; Loading @@ -46,7 +43,6 @@ import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.Transfer; import org.slf4j.Logger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.stereotype.Component; Loading @@ -64,9 +60,6 @@ public class VOSpaceClient { private final String baseUrl; private final String baseUrl; private final ForkJoinPool jaxbExecutor; private final ForkJoinPool jaxbExecutor; @Autowired protected HttpServletRequest servletRequest; public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) { public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) { if (backendUrl.endsWith("/")) { if (backendUrl.endsWith("/")) { // Remove final slash from configured URL // Remove final slash from configured URL Loading @@ -82,22 +75,18 @@ public class VOSpaceClient { .build(); .build(); } } public Node getNode(String path) { public Node getNode(String path, Optional<String> token) { return getNode(path, Optional.empty()); } public Node getNode(String path, Optional<String> adminToken) { HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken) HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .build(); .build(); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } } public JobSummary startTransferJob(Transfer transfer) { public JobSummary startTransferJob(Transfer transfer, Optional<String> token) { HttpRequest request = getRequest("/transfers?PHASE=RUN") HttpRequest request = getRequest("/transfers?PHASE=RUN", token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) Loading @@ -106,9 +95,9 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class)); } } public String getFileServiceEndpoint(Transfer transfer) { public String getFileServiceEndpoint(Transfer transfer, Optional<String> token) { HttpRequest request = getRequest("/synctrans") HttpRequest request = getRequest("/synctrans", token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) Loading @@ -131,7 +120,7 @@ public class VOSpaceClient { Matcher matcher = pattern.matcher(url); Matcher matcher = pattern.matcher(url); if (matcher.matches()) { if (matcher.matches()) { String jobId = matcher.group(1); String jobId = matcher.group(1); String errorDetail = getErrorDetail(jobId); String errorDetail = getErrorDetail(jobId, token); if (!errorDetail.isBlank()) { if (!errorDetail.isBlank()) { throw new BadRequestException(errorDetail); throw new BadRequestException(errorDetail); } } Loading @@ -142,15 +131,11 @@ public class VOSpaceClient { return protocols.get(0).getEndpoint(); return protocols.get(0).getEndpoint(); } } public Node createNode(Node node) { public Node createNode(Node node, Optional<String> token) { return createNode(node, Optional.empty()); } public Node createNode(Node node, Optional<String> adminToken) { String path = NodeUtils.getVosPath(node); String path = NodeUtils.getVosPath(node); HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken) HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .PUT(HttpRequest.BodyPublishers.ofString(marshal(node))) .PUT(HttpRequest.BodyPublishers.ofString(marshal(node))) Loading @@ -159,9 +144,9 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } } public void deleteNode(String path) { public void deleteNode(String path, Optional<String> token) { HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .DELETE() .DELETE() Loading @@ -170,15 +155,11 @@ public class VOSpaceClient { call(request, BodyHandlers.ofInputStream(), 200, res -> null); call(request, BodyHandlers.ofInputStream(), 200, res -> null); } } public Node setNode(Node node, boolean recursive) { public Node setNode(Node node, boolean recursive, Optional<String> token) { return setNode(node, recursive, Optional.empty()); } public Node setNode(Node node, boolean recursive, Optional<String> adminToken) { String path = NodeUtils.getVosPath(node); String path = NodeUtils.getVosPath(node); HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive, adminToken) HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive, token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(node))) .POST(HttpRequest.BodyPublishers.ofString(marshal(node))) Loading @@ -187,20 +168,20 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } } public List<Job> getAsyncRecallJobs() { public List<Job> getAsyncRecallJobs(Optional<String> token) { return getJobs("direction=pullToVoSpace", Job.JobType.ASYNC_RECALL); return getJobs("direction=pullToVoSpace", Job.JobType.ASYNC_RECALL, token); } } public List<Job> getArchiveJobs() { public List<Job> getArchiveJobs(Optional<String> token) { return getJobs("direction=pullFromVoSpace" return getJobs("direction=pullFromVoSpace" + "&VIEW=ivo://ia2.inaf.it/vospace/views%23tar" + "&VIEW=ivo://ia2.inaf.it/vospace/views%23tar" + "&VIEW=ivo://ia2.inaf.it/vospace/views%23zip", + "&VIEW=ivo://ia2.inaf.it/vospace/views%23zip", Job.JobType.ARCHIVE); Job.JobType.ARCHIVE, token); } } private List<Job> getJobs(String queryString, Job.JobType type) { private List<Job> getJobs(String queryString, Job.JobType type, Optional<String> token) { HttpRequest request = getRequest("/transfers?" + queryString) HttpRequest request = getRequest("/transfers?" + queryString, token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .GET() .GET() Loading @@ -213,25 +194,25 @@ public class VOSpaceClient { }); }); } } public String getArchiveJobHref(String jobId) { public String getArchiveJobHref(String jobId, Optional<String> token) { List<Protocol> protocols = getTransferDetails(jobId).getProtocols(); List<Protocol> protocols = getTransferDetails(jobId, token).getProtocols(); if (!protocols.isEmpty()) { if (!protocols.isEmpty()) { return protocols.get(0).getEndpoint(); return protocols.get(0).getEndpoint(); } } return null; return null; } } private Transfer getTransferDetails(String jobId) { private Transfer getTransferDetails(String jobId, Optional<String> token) { HttpRequest request = getRequest("/transfers/" + jobId + "/results/transferDetails") HttpRequest request = getRequest("/transfers/" + jobId + "/results/transferDetails", token) .GET().build(); .GET().build(); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class)); } } public ExecutionPhase getJobPhase(String jobId) { public ExecutionPhase getJobPhase(String jobId, Optional<String> token) { HttpRequest request = getRequest("/transfers/" + jobId + "/phase") HttpRequest request = getRequest("/transfers/" + jobId + "/phase", token) .GET() .GET() .build(); .build(); Loading @@ -244,9 +225,9 @@ public class VOSpaceClient { }); }); } } public String getErrorDetail(String jobId) { public String getErrorDetail(String jobId, Optional<String> token) { HttpRequest request = getRequest("/transfers/" + jobId + "/error") HttpRequest request = getRequest("/transfers/" + jobId + "/error", token) .header("Accept", "text/plain") .header("Accept", "text/plain") .GET() .GET() .build(); .build(); Loading Loading @@ -291,30 +272,14 @@ public class VOSpaceClient { } } } } private HttpRequest.Builder getRequest(String path) { private HttpRequest.Builder getRequest(String path, Optional<String> token) { return getRequest(path, Optional.empty()); } private HttpRequest.Builder getRequest(String path, Optional<String> adminToken) { HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + path)); HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + path)); String token = adminToken.orElseGet(() -> getToken()); if (token.isPresent()) { if (token != null) { builder.setHeader("Authorization", "Bearer " + token.get()); builder.setHeader("Authorization", "Bearer " + token); } } return builder; return builder; } } private String getToken() { HttpSession session = servletRequest.getSession(false); if (session != null) { User user = (User) session.getAttribute("user_data"); if (user != null) { return user.getAccessToken(); } } return null; } private <T> T unmarshal(InputStream in, Class<T> type) { private <T> T unmarshal(InputStream in, Class<T> type) { try { try { if (useJson) { if (useJson) { Loading vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java +19 −6 Original line number Original line Diff line number Diff line Loading @@ -5,6 +5,7 @@ */ */ package it.inaf.ia2.vospace.ui.controller; package it.inaf.ia2.vospace.ui.controller; import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import it.inaf.ia2.vospace.ui.exception.BadRequestException; Loading @@ -14,7 +15,9 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URL; import java.util.ArrayList; import java.util.ArrayList; import java.util.List; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.LinkNode; import org.slf4j.Logger; import org.slf4j.Logger; Loading @@ -40,10 +43,18 @@ public class CreateLinksController extends BaseController { @Autowired @Autowired private VOSpaceClient client; private VOSpaceClient client; @Autowired private TokenProvider tokenProvider; @Autowired private Executor requestsExecutor; @PostMapping(value = "/createLink", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/createLink", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<?> createLink(@RequestBody CreateLinkRequest request) { public ResponseEntity<?> createLink(@RequestBody CreateLinkRequest request) { ContainerNode parent = getFolder(request.getFolder()); Optional<String> token = tokenProvider.getToken(); ContainerNode parent = getFolder(request.getFolder(), token); String uri = parent.getUri() + "/" + request.getNodeName(); String uri = parent.getUri() + "/" + request.getNodeName(); Loading @@ -51,7 +62,7 @@ public class CreateLinksController extends BaseController { link.setUri(uri); link.setUri(uri); link.setTarget(request.getUrl()); link.setTarget(request.getUrl()); client.createNode(link); client.createNode(link, token); return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build(); } } Loading @@ -60,7 +71,9 @@ public class CreateLinksController extends BaseController { public ResponseEntity<?> uploadLinks(@RequestParam(value = "file", required = true) MultipartFile file, public ResponseEntity<?> uploadLinks(@RequestParam(value = "file", required = true) MultipartFile file, @RequestParam("folder") String folder) throws IOException { @RequestParam("folder") String folder) throws IOException { ContainerNode parent = getFolder(folder); Optional<String> token = tokenProvider.getToken(); ContainerNode parent = getFolder(folder, token); String fileContent = new String(file.getBytes()); String fileContent = new String(file.getBytes()); Loading Loading @@ -90,7 +103,7 @@ public class CreateLinksController extends BaseController { httpCallsGroups.add(currentHttpCallsGroup); httpCallsGroups.add(currentHttpCallsGroup); } } currentHttpCallsGroup.add(CompletableFuture.supplyAsync(() -> client.createNode(link), Runnable::run)); currentHttpCallsGroup.add(CompletableFuture.supplyAsync(() -> client.createNode(link, token), requestsExecutor)); } } } } Loading Loading @@ -123,9 +136,9 @@ public class CreateLinksController extends BaseController { } } } } private ContainerNode getFolder(String folderPath) { private ContainerNode getFolder(String folderPath, Optional<String> token) { try { try { return (ContainerNode) client.getNode("/" + folderPath); return (ContainerNode) client.getNode("/" + folderPath, token); } catch (VOSpaceStatusException ex) { } catch (VOSpaceStatusException ex) { if (ex.getHttpStatus() == 404) { if (ex.getHttpStatus() == 404) { throw new BadRequestException("Folder parameter specified a non-existent folder: /" + folderPath); throw new BadRequestException("Folder parameter specified a non-existent folder: /" + folderPath); Loading Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SessionThreadFactory.java 0 → 100644 +53 −0 Original line number Original line 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; import java.util.concurrent.ThreadFactory; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * VOSpaceClient needs to extract the access token stored into the HTTP session * and forward it to the VOSpace REST service. The client can retrieve the * session from the autowired HttpServletRequest, however this doesn't work if * the client is called from a standard new thread (for example when the client * is invoked from a CompletableFuture), because Spring Context is missing and * retrieval of the autowired HttpServletRequest proxy fails with the "No * thread-bound request found" exception. Many CompletableFuture methods accept * an Executor as parameter, so this ThreadFactory has been created in order to * store the session in these particular threads. An Executor using this * ThreadaFactory must be passed to the CompletableFuture. An alternative would * be passing the original HttpServletRequest to all VOSpaceClient methods. A * first attempt using the current executor (Runnable::run) shown that tasks * where executed sequentially, so this dedicated Executor has been set up. */ public class SessionThreadFactory implements ThreadFactory { private final HttpServletRequest request; public SessionThreadFactory(HttpServletRequest request) { this.request = request; } @Override public Thread newThread(Runnable runnable) { return new SessionThread(runnable, request); } public static class SessionThread extends Thread { private final HttpSession session; public SessionThread(Runnable runnable, HttpServletRequest request) { super(runnable); this.session = request.getSession(false); } public HttpSession getHttpSession() { return this.session; } } }
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/TokenProvider.java 0 → 100644 +32 −0 Original line number Original line 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; import it.inaf.ia2.aa.data.User; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class TokenProvider { @Autowired private HttpServletRequest request; public Optional<String> getToken() { HttpSession session = request.getSession(false); if (session != null) { User user = (User) session.getAttribute("user_data"); if (user != null) { return Optional.of(user.getAccessToken()); } } return Optional.empty(); } }
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java +12 −0 Original line number Original line Diff line number Diff line Loading @@ -12,6 +12,8 @@ import it.inaf.ia2.aa.ServletRapClient; import it.inaf.ia2.aa.UserManager; import it.inaf.ia2.aa.UserManager; import it.inaf.ia2.gms.client.GmsClient; import it.inaf.ia2.gms.client.GmsClient; import it.inaf.ia2.rap.client.ClientCredentialsRapClient; import it.inaf.ia2.rap.client.ClientCredentialsRapClient; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; Loading Loading @@ -66,6 +68,16 @@ public class VOSpaceUiApplication { return new ForkJoinPool(parallelism, threadFactory, null, false); return new ForkJoinPool(parallelism, threadFactory, null, false); } } /** * Executor to pass to CompletableFuture methods to avoid * "RejectedExecutionException: Thread limit exceeded replacing blocked * worker". */ @Bean public Executor requestsExecutor() { return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); } @Bean @Bean public RestTemplate restTemplate() { public RestTemplate restTemplate() { return new RestTemplate(); return new RestTemplate(); Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +30 −65 Original line number Original line Diff line number Diff line Loading @@ -6,7 +6,6 @@ package it.inaf.ia2.vospace.ui.client; package it.inaf.ia2.vospace.ui.client; import com.fasterxml.jackson.databind.ObjectMapper; 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.VOSpaceUiApplication; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import it.inaf.ia2.vospace.ui.exception.BadRequestException; Loading Loading @@ -35,8 +34,6 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.xml.bind.JAXB; import javax.xml.bind.JAXB; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.uws.v1.JobSummary; Loading @@ -46,7 +43,6 @@ import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.Transfer; import org.slf4j.Logger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.stereotype.Component; Loading @@ -64,9 +60,6 @@ public class VOSpaceClient { private final String baseUrl; private final String baseUrl; private final ForkJoinPool jaxbExecutor; private final ForkJoinPool jaxbExecutor; @Autowired protected HttpServletRequest servletRequest; public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) { public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) { if (backendUrl.endsWith("/")) { if (backendUrl.endsWith("/")) { // Remove final slash from configured URL // Remove final slash from configured URL Loading @@ -82,22 +75,18 @@ public class VOSpaceClient { .build(); .build(); } } public Node getNode(String path) { public Node getNode(String path, Optional<String> token) { return getNode(path, Optional.empty()); } public Node getNode(String path, Optional<String> adminToken) { HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken) HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .build(); .build(); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } } public JobSummary startTransferJob(Transfer transfer) { public JobSummary startTransferJob(Transfer transfer, Optional<String> token) { HttpRequest request = getRequest("/transfers?PHASE=RUN") HttpRequest request = getRequest("/transfers?PHASE=RUN", token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) Loading @@ -106,9 +95,9 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class)); } } public String getFileServiceEndpoint(Transfer transfer) { public String getFileServiceEndpoint(Transfer transfer, Optional<String> token) { HttpRequest request = getRequest("/synctrans") HttpRequest request = getRequest("/synctrans", token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) Loading @@ -131,7 +120,7 @@ public class VOSpaceClient { Matcher matcher = pattern.matcher(url); Matcher matcher = pattern.matcher(url); if (matcher.matches()) { if (matcher.matches()) { String jobId = matcher.group(1); String jobId = matcher.group(1); String errorDetail = getErrorDetail(jobId); String errorDetail = getErrorDetail(jobId, token); if (!errorDetail.isBlank()) { if (!errorDetail.isBlank()) { throw new BadRequestException(errorDetail); throw new BadRequestException(errorDetail); } } Loading @@ -142,15 +131,11 @@ public class VOSpaceClient { return protocols.get(0).getEndpoint(); return protocols.get(0).getEndpoint(); } } public Node createNode(Node node) { public Node createNode(Node node, Optional<String> token) { return createNode(node, Optional.empty()); } public Node createNode(Node node, Optional<String> adminToken) { String path = NodeUtils.getVosPath(node); String path = NodeUtils.getVosPath(node); HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken) HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .PUT(HttpRequest.BodyPublishers.ofString(marshal(node))) .PUT(HttpRequest.BodyPublishers.ofString(marshal(node))) Loading @@ -159,9 +144,9 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } } public void deleteNode(String path) { public void deleteNode(String path, Optional<String> token) { HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .DELETE() .DELETE() Loading @@ -170,15 +155,11 @@ public class VOSpaceClient { call(request, BodyHandlers.ofInputStream(), 200, res -> null); call(request, BodyHandlers.ofInputStream(), 200, res -> null); } } public Node setNode(Node node, boolean recursive) { public Node setNode(Node node, boolean recursive, Optional<String> token) { return setNode(node, recursive, Optional.empty()); } public Node setNode(Node node, boolean recursive, Optional<String> adminToken) { String path = NodeUtils.getVosPath(node); String path = NodeUtils.getVosPath(node); HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive, adminToken) HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive, token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(node))) .POST(HttpRequest.BodyPublishers.ofString(marshal(node))) Loading @@ -187,20 +168,20 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } } public List<Job> getAsyncRecallJobs() { public List<Job> getAsyncRecallJobs(Optional<String> token) { return getJobs("direction=pullToVoSpace", Job.JobType.ASYNC_RECALL); return getJobs("direction=pullToVoSpace", Job.JobType.ASYNC_RECALL, token); } } public List<Job> getArchiveJobs() { public List<Job> getArchiveJobs(Optional<String> token) { return getJobs("direction=pullFromVoSpace" return getJobs("direction=pullFromVoSpace" + "&VIEW=ivo://ia2.inaf.it/vospace/views%23tar" + "&VIEW=ivo://ia2.inaf.it/vospace/views%23tar" + "&VIEW=ivo://ia2.inaf.it/vospace/views%23zip", + "&VIEW=ivo://ia2.inaf.it/vospace/views%23zip", Job.JobType.ARCHIVE); Job.JobType.ARCHIVE, token); } } private List<Job> getJobs(String queryString, Job.JobType type) { private List<Job> getJobs(String queryString, Job.JobType type, Optional<String> token) { HttpRequest request = getRequest("/transfers?" + queryString) HttpRequest request = getRequest("/transfers?" + queryString, token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .GET() .GET() Loading @@ -213,25 +194,25 @@ public class VOSpaceClient { }); }); } } public String getArchiveJobHref(String jobId) { public String getArchiveJobHref(String jobId, Optional<String> token) { List<Protocol> protocols = getTransferDetails(jobId).getProtocols(); List<Protocol> protocols = getTransferDetails(jobId, token).getProtocols(); if (!protocols.isEmpty()) { if (!protocols.isEmpty()) { return protocols.get(0).getEndpoint(); return protocols.get(0).getEndpoint(); } } return null; return null; } } private Transfer getTransferDetails(String jobId) { private Transfer getTransferDetails(String jobId, Optional<String> token) { HttpRequest request = getRequest("/transfers/" + jobId + "/results/transferDetails") HttpRequest request = getRequest("/transfers/" + jobId + "/results/transferDetails", token) .GET().build(); .GET().build(); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class)); } } public ExecutionPhase getJobPhase(String jobId) { public ExecutionPhase getJobPhase(String jobId, Optional<String> token) { HttpRequest request = getRequest("/transfers/" + jobId + "/phase") HttpRequest request = getRequest("/transfers/" + jobId + "/phase", token) .GET() .GET() .build(); .build(); Loading @@ -244,9 +225,9 @@ public class VOSpaceClient { }); }); } } public String getErrorDetail(String jobId) { public String getErrorDetail(String jobId, Optional<String> token) { HttpRequest request = getRequest("/transfers/" + jobId + "/error") HttpRequest request = getRequest("/transfers/" + jobId + "/error", token) .header("Accept", "text/plain") .header("Accept", "text/plain") .GET() .GET() .build(); .build(); Loading Loading @@ -291,30 +272,14 @@ public class VOSpaceClient { } } } } private HttpRequest.Builder getRequest(String path) { private HttpRequest.Builder getRequest(String path, Optional<String> token) { return getRequest(path, Optional.empty()); } private HttpRequest.Builder getRequest(String path, Optional<String> adminToken) { HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + path)); HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + path)); String token = adminToken.orElseGet(() -> getToken()); if (token.isPresent()) { if (token != null) { builder.setHeader("Authorization", "Bearer " + token.get()); builder.setHeader("Authorization", "Bearer " + token); } } return builder; return builder; } } private String getToken() { HttpSession session = servletRequest.getSession(false); if (session != null) { User user = (User) session.getAttribute("user_data"); if (user != null) { return user.getAccessToken(); } } return null; } private <T> T unmarshal(InputStream in, Class<T> type) { private <T> T unmarshal(InputStream in, Class<T> type) { try { try { if (useJson) { if (useJson) { Loading
vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java +19 −6 Original line number Original line Diff line number Diff line Loading @@ -5,6 +5,7 @@ */ */ package it.inaf.ia2.vospace.ui.controller; package it.inaf.ia2.vospace.ui.controller; import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import it.inaf.ia2.vospace.ui.exception.BadRequestException; Loading @@ -14,7 +15,9 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URL; import java.util.ArrayList; import java.util.ArrayList; import java.util.List; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.LinkNode; import org.slf4j.Logger; import org.slf4j.Logger; Loading @@ -40,10 +43,18 @@ public class CreateLinksController extends BaseController { @Autowired @Autowired private VOSpaceClient client; private VOSpaceClient client; @Autowired private TokenProvider tokenProvider; @Autowired private Executor requestsExecutor; @PostMapping(value = "/createLink", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/createLink", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<?> createLink(@RequestBody CreateLinkRequest request) { public ResponseEntity<?> createLink(@RequestBody CreateLinkRequest request) { ContainerNode parent = getFolder(request.getFolder()); Optional<String> token = tokenProvider.getToken(); ContainerNode parent = getFolder(request.getFolder(), token); String uri = parent.getUri() + "/" + request.getNodeName(); String uri = parent.getUri() + "/" + request.getNodeName(); Loading @@ -51,7 +62,7 @@ public class CreateLinksController extends BaseController { link.setUri(uri); link.setUri(uri); link.setTarget(request.getUrl()); link.setTarget(request.getUrl()); client.createNode(link); client.createNode(link, token); return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build(); } } Loading @@ -60,7 +71,9 @@ public class CreateLinksController extends BaseController { public ResponseEntity<?> uploadLinks(@RequestParam(value = "file", required = true) MultipartFile file, public ResponseEntity<?> uploadLinks(@RequestParam(value = "file", required = true) MultipartFile file, @RequestParam("folder") String folder) throws IOException { @RequestParam("folder") String folder) throws IOException { ContainerNode parent = getFolder(folder); Optional<String> token = tokenProvider.getToken(); ContainerNode parent = getFolder(folder, token); String fileContent = new String(file.getBytes()); String fileContent = new String(file.getBytes()); Loading Loading @@ -90,7 +103,7 @@ public class CreateLinksController extends BaseController { httpCallsGroups.add(currentHttpCallsGroup); httpCallsGroups.add(currentHttpCallsGroup); } } currentHttpCallsGroup.add(CompletableFuture.supplyAsync(() -> client.createNode(link), Runnable::run)); currentHttpCallsGroup.add(CompletableFuture.supplyAsync(() -> client.createNode(link, token), requestsExecutor)); } } } } Loading Loading @@ -123,9 +136,9 @@ public class CreateLinksController extends BaseController { } } } } private ContainerNode getFolder(String folderPath) { private ContainerNode getFolder(String folderPath, Optional<String> token) { try { try { return (ContainerNode) client.getNode("/" + folderPath); return (ContainerNode) client.getNode("/" + folderPath, token); } catch (VOSpaceStatusException ex) { } catch (VOSpaceStatusException ex) { if (ex.getHttpStatus() == 404) { if (ex.getHttpStatus() == 404) { throw new BadRequestException("Folder parameter specified a non-existent folder: /" + folderPath); throw new BadRequestException("Folder parameter specified a non-existent folder: /" + folderPath); Loading