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