From 55825cc666551376211e85df2990f6259603f03c Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Wed, 2 Dec 2020 19:37:53 +0100
Subject: [PATCH] Solved JAXB ClassLoader issue. Added VOSpaceClientTest

---
 vospace-ui-backend/pom.xml                    |  7 +-
 .../ui/JaxbForkJoinWorkerThreadFactory.java   | 32 ++++++++
 .../ia2/vospace/ui/VOSpaceUiApplication.java  | 10 +++
 .../ia2/vospace/ui/client/VOSpaceClient.java  |  7 +-
 .../vospace/ui/client/VOSpaceClientTest.java  | 80 +++++++++++++++++++
 .../src/test/resources/nodes-response.xml     | 25 ++++++
 6 files changed, 159 insertions(+), 2 deletions(-)
 create mode 100644 vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/JaxbForkJoinWorkerThreadFactory.java
 create mode 100644 vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java
 create mode 100644 vospace-ui-backend/src/test/resources/nodes-response.xml

diff --git a/vospace-ui-backend/pom.xml b/vospace-ui-backend/pom.xml
index 27cdcf9..1eb1bba 100644
--- a/vospace-ui-backend/pom.xml
+++ b/vospace-ui-backend/pom.xml
@@ -16,6 +16,7 @@
 
     <properties>
         <java.version>14</java.version>
+        <mockito.version>3.5.13</mockito.version>
     </properties>
 
     <dependencies>
@@ -28,7 +29,6 @@
             <artifactId>vospace-datamodel</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>
-
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-devtools</artifactId>
@@ -40,6 +40,11 @@
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/JaxbForkJoinWorkerThreadFactory.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/JaxbForkJoinWorkerThreadFactory.java
new file mode 100644
index 0000000..dbd4c4a
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/JaxbForkJoinWorkerThreadFactory.java
@@ -0,0 +1,32 @@
+package it.inaf.ia2.vospace.ui;
+
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory;
+import java.util.concurrent.ForkJoinWorkerThread;
+
+/**
+ * This class solves a ClassLoader issue with newer versions of Java. See this
+ * post: https://stackoverflow.com/a/61012531/771431
+ */
+public class JaxbForkJoinWorkerThreadFactory implements ForkJoinWorkerThreadFactory {
+
+    private final ClassLoader classLoader;
+
+    public JaxbForkJoinWorkerThreadFactory() {
+        classLoader = Thread.currentThread().getContextClassLoader();
+    }
+
+    @Override
+    public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
+        ForkJoinWorkerThread thread = new JaxbForkJoinWorkerThread(pool);
+        thread.setContextClassLoader(classLoader);
+        return thread;
+    }
+
+    private static class JaxbForkJoinWorkerThread extends ForkJoinWorkerThread {
+
+        private JaxbForkJoinWorkerThread(ForkJoinPool pool) {
+            super(pool);
+        }
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
index 010de2f..404e4b0 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
@@ -1,5 +1,6 @@
 package it.inaf.ia2.vospace.ui;
 
+import java.util.concurrent.ForkJoinPool;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@@ -9,4 +10,13 @@ public class VOSpaceUiApplication {
     public static void main(String[] args) {
         SpringApplication.run(VOSpaceUiApplication.class, args);
     }
+
+    /**
+     * Solves a ClassLoader issue. See class JaxbForkJoinWorkerThreadFactory.
+     */
+    public static ForkJoinPool getJaxbExecutor() {
+        JaxbForkJoinWorkerThreadFactory threadFactory = new JaxbForkJoinWorkerThreadFactory();
+        int parallelism = Math.min(0x7fff /* copied from ForkJoinPool.java */, Runtime.getRuntime().availableProcessors());
+        return new ForkJoinPool(parallelism, threadFactory, null, false);
+    }
 }
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 31e0008..1628d5c 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
@@ -2,6 +2,7 @@ package it.inaf.ia2.vospace.ui.client;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import it.inaf.ia2.vospace.ui.VOSpaceException;
+import it.inaf.ia2.vospace.ui.VOSpaceUiApplication;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UncheckedIOException;
@@ -13,6 +14,7 @@ import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodyHandlers;
 import java.util.Scanner;
 import java.util.concurrent.CompletionException;
+import java.util.concurrent.ForkJoinPool;
 import java.util.function.Function;
 import javax.xml.bind.JAXB;
 import net.ivoa.xml.vospace.v2.Node;
@@ -33,6 +35,7 @@ public class VOSpaceClient {
 
     private final HttpClient httpClient;
     private final String baseUrl;
+    private final ForkJoinPool jaxbExecutor;
 
     public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) {
         if (backendUrl.endsWith("/")) {
@@ -41,6 +44,8 @@ public class VOSpaceClient {
         }
         baseUrl = backendUrl;
 
+        jaxbExecutor = VOSpaceUiApplication.getJaxbExecutor();
+
         httpClient = HttpClient.newBuilder()
                 .followRedirects(HttpClient.Redirect.ALWAYS)
                 .version(HttpClient.Version.HTTP_1_1)
@@ -66,7 +71,7 @@ public class VOSpaceClient {
                         logServerError(request, response);
                         throw new VOSpaceException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode());
                     })
-                    .thenApply(response -> responseHandler.apply(response))
+                    .thenApplyAsync(response -> responseHandler.apply(response), jaxbExecutor)
                     .join();
         } catch (CompletionException ex) {
             if (ex.getCause() != null) {
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
new file mode 100644
index 0000000..2149009
--- /dev/null
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java
@@ -0,0 +1,80 @@
+package it.inaf.ia2.vospace.ui.client;
+
+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.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import net.ivoa.xml.vospace.v2.ContainerNode;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+@ExtendWith(MockitoExtension.class)
+public class VOSpaceClientTest {
+
+    private HttpClient mockedHttpClient;
+    private VOSpaceClient voSpaceClient;
+
+    @BeforeEach
+    public void init() {
+        mockedHttpClient = mock(HttpClient.class);
+
+        HttpClient.Builder builder = mock(HttpClient.Builder.class);
+        when(builder.followRedirects(any())).thenReturn(builder);
+        when(builder.version(any())).thenReturn(builder);
+        when(builder.build()).thenReturn(mockedHttpClient);
+
+        try ( MockedStatic<HttpClient> staticMock = Mockito.mockStatic(HttpClient.class)) {
+            staticMock.when(HttpClient::newBuilder).thenReturn(builder);
+            voSpaceClient = new VOSpaceClient("http://localhost/vospace");
+        }
+    }
+
+    @Test
+    public void testGetXmlNode() {
+        ReflectionTestUtils.setField(voSpaceClient, "useJson", false);
+
+        CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("nodes-response.xml"));
+        when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response);
+
+        ContainerNode node = (ContainerNode) voSpaceClient.getNode("/node1");
+        assertEquals("vos://ia2.inaf.it!vospace/node1", node.getUri());
+    }
+
+    protected static String getResourceFileContent(String fileName) {
+        try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) {
+            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    protected static CompletableFuture<HttpResponse<InputStream>> getMockedStreamResponseFuture(int statusCode, String body) {
+        return CompletableFuture.completedFuture(getMockedStreamResponse(200, body));
+    }
+
+    protected static HttpResponse<InputStream> getMockedStreamResponse(int statusCode, String body) {
+        HttpResponse response = getMockedResponse(statusCode);
+        InputStream in = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
+        when(response.body()).thenReturn(in);
+        return response;
+    }
+
+    protected static HttpResponse getMockedResponse(int statusCode) {
+        HttpResponse response = mock(HttpResponse.class);
+        when(response.statusCode()).thenReturn(statusCode);
+        return response;
+    }
+}
diff --git a/vospace-ui-backend/src/test/resources/nodes-response.xml b/vospace-ui-backend/src/test/resources/nodes-response.xml
new file mode 100644
index 0000000..3c8ea0f
--- /dev/null
+++ b/vospace-ui-backend/src/test/resources/nodes-response.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<vos:node xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" uri="vos://ia2.inaf.it!vospace/node1" xsi:type="vos:ContainerNode">
+    <vos:properties>
+        <vos:property uri="ivo://ivoa.net/vospace/core#length" readOnly="true">483282612224</vos:property>
+        <vos:property uri="ivo://ivoa.net/vospace/core#date" readOnly="true">2018-11-23T19:30:03.387</vos:property>
+        <vos:property uri="ivo://ivoa.net/vospace/core#groupread" readOnly="false">ivo://ia2.inaf.it/gms#group1</vos:property>
+        <vos:property uri="ivo://ivoa.net/vospace/core#groupwrite" readOnly="false">ivo://ia2.inaf.it/gms#group1</vos:property>
+        <vos:property uri="ivo://ivoa.net/vospace/core#ispublic" readOnly="false">true</vos:property>
+        <vos:property uri="ivo://ivoa.net/vospace/core#creator" readOnly="false">123</vos:property>
+        <vos:property uri="ivo://ivoa.net/vospace/core#quota" readOnly="false">3298534883328</vos:property>
+    </vos:properties>
+    <vos:nodes>
+        <vos:node uri="vos://ia2.inaf.it!vospace/node1/node2" xsi:type="vos:ContainerNode">
+            <vos:properties>
+                <vos:property uri="ivo://ivoa.net/vospace/core#length" readOnly="true">483282612224</vos:property>
+                <vos:property uri="ivo://ivoa.net/vospace/core#date" readOnly="true">2018-11-23T19:30:03.350</vos:property>
+                <vos:property uri="ivo://ivoa.net/vospace/core#groupread" readOnly="false">ivo://ia2.inaf.it/gms#group1</vos:property>
+                <vos:property uri="ivo://ivoa.net/vospace/core#groupwrite" readOnly="false">ivo://ia2.inaf.it/gms#group1</vos:property>
+                <vos:property uri="ivo://ivoa.net/vospace/core#ispublic" readOnly="false">true</vos:property>
+                <vos:property uri="ivo://ivoa.net/vospace/core#creator" readOnly="false">123</vos:property>
+            </vos:properties>
+            <vos:nodes />
+        </vos:node>
+    </vos:nodes>
+</vos:node>
-- 
GitLab