diff --git a/vospace-ui-backend/pom.xml b/vospace-ui-backend/pom.xml
index 27cdcf97aa333bbb9456cc8bf166f569b591864d..1eb1bbac1d8d3fd816760674e70ae8feebb8ac81 100644
--- a/vospace-ui-backend/pom.xml
+++ b/vospace-ui-backend/pom.xml
@@ -16,6 +16,7 @@
14
+ 3.5.13
@@ -28,7 +29,6 @@
vospace-datamodel
1.0-SNAPSHOT
-
org.springframework.boot
spring-boot-devtools
@@ -40,6 +40,11 @@
spring-boot-starter-test
test
+
+ org.mockito
+ mockito-inline
+ test
+
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 0000000000000000000000000000000000000000..dbd4c4af5f25c2d9016e76bf66dbc4b1277f3110
--- /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 010de2fa4abd09bf7dc2a68c10b09a4c19e36ce0..404e4b02f3b07beb831330df2c76534ca23be749 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 31e00082d2c8fb1913d2a8319b78730c28eb1653..1628d5cf22fed8ac98d90a5d04e4ade14d866005 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 0000000000000000000000000000000000000000..2149009f110b16194beeb4c79eed3c1780ca8f15
--- /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 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> getMockedStreamResponseFuture(int statusCode, String body) {
+ return CompletableFuture.completedFuture(getMockedStreamResponse(200, body));
+ }
+
+ protected static HttpResponse 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 0000000000000000000000000000000000000000..3c8ea0ff2c5443c38b043959b59e5f56eab1ba0e
--- /dev/null
+++ b/vospace-ui-backend/src/test/resources/nodes-response.xml
@@ -0,0 +1,25 @@
+
+
+
+ 483282612224
+ 2018-11-23T19:30:03.387
+ ivo://ia2.inaf.it/gms#group1
+ ivo://ia2.inaf.it/gms#group1
+ true
+ 123
+ 3298534883328
+
+
+
+
+ 483282612224
+ 2018-11-23T19:30:03.350
+ ivo://ia2.inaf.it/gms#group1
+ ivo://ia2.inaf.it/gms#group1
+ true
+ 123
+
+
+
+
+