Commit cdb2de60 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Passed token as parameter instead of using @Autowired of HttpSession to avoid...

Passed token as parameter instead of using @Autowired of HttpSession to avoid issues with threads; defined custom Executor
parent 100137ca
Loading
Loading
Loading
Loading
Loading
+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;
        }
    }
}
+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();
    }
}
+12 −0
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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();
+30 −65
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;


@@ -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
@@ -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)))
@@ -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)))
@@ -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);
                    }
                    }
@@ -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)))
@@ -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()
@@ -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)))
@@ -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()
@@ -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();


@@ -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();
@@ -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) {
+19 −6
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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;
@@ -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();


@@ -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();
    }
    }
@@ -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());


@@ -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));
            }
            }
        }
        }


@@ -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