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