From c908e2e5df11cc11601953eff407994b40ad89f2 Mon Sep 17 00:00:00 2001 From: Neena Jacob Date: Wed, 1 Oct 2025 13:57:54 +0530 Subject: [PATCH 1/2] Added changes to have multipart sync & async call using actual server --- .../rest/client/tck/EntityPartTest.java | 105 +++++++---- .../tck/asynctests/AsyncEntityPartTest.java | 176 ++++++++++++++++++ 2 files changed, 243 insertions(+), 38 deletions(-) create mode 100644 tck/src/main/java/org/eclipse/microprofile/rest/client/tck/asynctests/AsyncEntityPartTest.java diff --git a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/EntityPartTest.java b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/EntityPartTest.java index 1e02c5c1..bc35f735 100644 --- a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/EntityPartTest.java +++ b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/EntityPartTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Contributors to the Eclipse Foundation + * Copyright 2025 Contributors to the Eclipse Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,6 +32,7 @@ import org.eclipse.microprofile.rest.client.RestClientBuilder; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.arquillian.testng.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.EmptyAsset; @@ -43,13 +46,12 @@ import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonValue; +import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.client.ClientRequestContext; -import jakarta.ws.rs.client.ClientRequestFilter; import jakarta.ws.rs.core.EntityPart; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -60,12 +62,44 @@ @RunAsClient public class EntityPartTest extends Arquillian { + @ArquillianResource + private URI uri; + @Deployment public static WebArchive createDeployment() { return ShrinkWrap.create(WebArchive.class, EntityPart.class.getSimpleName() + ".war") + .addClasses(FileUploadResource.class, FileUploadApplication.class) .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); } + @ApplicationPath("/") + public static class FileUploadApplication extends jakarta.ws.rs.core.Application { + } + + @Path("/entitypart") + public static class FileUploadResource { + + @POST + @Path("upload") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + public Response uploadFile(List entityParts) throws IOException { + final JsonArrayBuilder jsonBuilder = Json.createArrayBuilder(); + for (EntityPart part : entityParts) { + final JsonObjectBuilder jsonPartBuilder = Json.createObjectBuilder(); + jsonPartBuilder.add("name", part.getName()); + if (part.getFileName().isPresent()) { + jsonPartBuilder.add("fileName", part.getFileName().get()); + } else { + throw new BadRequestException("No file name for entity part " + part); + } + jsonPartBuilder.add("content", part.getContent(String.class)); + jsonBuilder.add(jsonPartBuilder); + } + return Response.status(201).entity(jsonBuilder.build()).build(); + } + } + /** * Tests that a single file is upload. The response is a simple JSON response with the file information. * @@ -153,16 +187,38 @@ public void uploadMultipleFiles() throws Exception { } } - private static FileManagerClient createClient() { - return RestClientBuilder.newBuilder() - // Fake URI as we use a filter to short-circuit the request - .baseUri("http://localhost:8080") - .register(new FileManagerFilter()) - .build(FileManagerClient.class); + private FileManagerClient createClient() { + try { + return RestClientBuilder.newBuilder() + .baseUri(createCombinedUri(uri, "entitypart")) + .build(FileManagerClient.class); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private static URI createCombinedUri(final URI uri, final String path) throws URISyntaxException { + if (path == null || path.isEmpty()) { + return uri; + } + String uriString = uri.toString(); + final StringBuilder builder = new StringBuilder(uriString); + if (builder.charAt(builder.length() - 1) == '/') { + if (path.charAt(0) == '/') { + builder.append(path.substring(1)); + } else { + builder.append(path); + } + } else if (path.charAt(0) == '/') { + builder.append(path); + } else { + builder.append('/').append(path); + } + return new URI(builder.toString()); } @Consumes(MediaType.MULTIPART_FORM_DATA) - @Produces(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) public interface FileManagerClient extends AutoCloseable { @POST @@ -170,31 +226,4 @@ public interface FileManagerClient extends AutoCloseable { Response uploadFile(List entityParts) throws IOException; } - public static class FileManagerFilter implements ClientRequestFilter { - - @Override - public void filter(final ClientRequestContext requestContext) throws IOException { - if (requestContext.getMethod().equals("POST")) { - // Download the file - @SuppressWarnings("unchecked") - final List entityParts = (List) requestContext.getEntity(); - final JsonArrayBuilder jsonBuilder = Json.createArrayBuilder(); - for (EntityPart part : entityParts) { - final JsonObjectBuilder jsonPartBuilder = Json.createObjectBuilder(); - jsonPartBuilder.add("name", part.getName()); - if (part.getFileName().isPresent()) { - jsonPartBuilder.add("fileName", part.getFileName().get()); - } else { - throw new BadRequestException("No file name for entity part " + part); - } - jsonPartBuilder.add("content", part.getContent(String.class)); - jsonBuilder.add(jsonPartBuilder); - } - requestContext.abortWith(Response.status(201).entity(jsonBuilder.build()).build()); - } else { - requestContext - .abortWith(Response.status(Response.Status.BAD_REQUEST).entity("Invalid request").build()); - } - } - } -} +} \ No newline at end of file diff --git a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/asynctests/AsyncEntityPartTest.java b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/asynctests/AsyncEntityPartTest.java new file mode 100644 index 00000000..bd495041 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/asynctests/AsyncEntityPartTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2025 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.microprofile.rest.client.tck.asynctests; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.arquillian.testng.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.testng.Assert; +import org.testng.annotations.Test; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.EntityPart; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * @author Neena Jacob + */ +@RunAsClient +public class AsyncEntityPartTest extends Arquillian { + + @ArquillianResource + private URI uri; + + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class, EntityPart.class.getSimpleName() + ".war") + .addClasses(FileUploadResource.class, FileUploadApplication.class) + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); + } + + @ApplicationPath("/") + public static class FileUploadApplication extends jakarta.ws.rs.core.Application { + } + + @Path("/entitypart") + public static class FileUploadResource { + + @POST + @Path("upload") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + public Response uploadFile(List entityParts) throws IOException { + final JsonArrayBuilder jsonBuilder = Json.createArrayBuilder(); + for (EntityPart part : entityParts) { + final JsonObjectBuilder jsonPartBuilder = Json.createObjectBuilder(); + jsonPartBuilder.add("name", part.getName()); + if (part.getFileName().isPresent()) { + jsonPartBuilder.add("fileName", part.getFileName().get()); + } else { + throw new BadRequestException("No file name for entity part " + part); + } + jsonPartBuilder.add("content", part.getContent(String.class)); + jsonBuilder.add(jsonPartBuilder); + } + return Response.status(201).entity(jsonBuilder.build()).build(); + } + } + + /** + * Tests that a single file is upload. The response is a simple JSON response with the file information. + * + * @throws Exception + * if a test error occurs + */ + @Test + public void uploadFileAsync() throws Exception { + try (AsyncFileManagerClient client = createClient()) { + final byte[] content; + try (InputStream in = AsyncEntityPartTest.class.getResourceAsStream("/multipart/test-file1.txt")) { + Assert.assertNotNull(in, "Could not find /multipart/test-file1.txt"); + content = in.readAllBytes(); + } + // Send in an InputStream to ensure it works with an InputStream + final List files = List.of(EntityPart.withFileName("test-file1.txt") + .content(new ByteArrayInputStream(content)) + .mediaType(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .build()); + + CompletionStage futureResponse = client.uploadFileAsync(files); + Response response = futureResponse.toCompletableFuture().get(); + + try { + Assert.assertEquals(201, response.getStatus()); + final JsonArray jsonArray = response.readEntity(JsonArray.class); + Assert.assertNotNull(jsonArray); + Assert.assertEquals(jsonArray.size(), 1); + final JsonObject json = jsonArray.getJsonObject(0); + Assert.assertEquals(json.getString("name"), "test-file1.txt"); + Assert.assertEquals(json.getString("fileName"), "test-file1.txt"); + Assert.assertEquals(json.getString("content"), "This is a test file for file 1.\n"); + } finally { + response.close(); + } + } + } + + private AsyncFileManagerClient createClient() { + try { + return RestClientBuilder.newBuilder() + .baseUri(createCombinedUri(uri, "entitypart")) + .build(AsyncFileManagerClient.class); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private static URI createCombinedUri(final URI uri, final String path) throws URISyntaxException { + if (path == null || path.isEmpty()) { + return uri; + } + String uriString = uri.toString(); + final StringBuilder builder = new StringBuilder(uriString); + if (builder.charAt(builder.length() - 1) == '/') { + if (path.charAt(0) == '/') { + builder.append(path.substring(1)); + } else { + builder.append(path); + } + } else if (path.charAt(0) == '/') { + builder.append(path); + } else { + builder.append('/').append(path); + } + return new URI(builder.toString()); + } + + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + public interface AsyncFileManagerClient extends AutoCloseable { + + @POST + @Path("upload") + CompletionStage uploadFileAsync(List entityParts); + } + +} From e3a2cb734cd84fb460f5ec3d559073a858236352 Mon Sep 17 00:00:00 2001 From: Neena Jacob Date: Mon, 13 Oct 2025 14:36:57 +0530 Subject: [PATCH 2/2] Code refactored --- .../rest/client/tck/EntityPartTest.java | 34 +++---------------- .../tck/asynctests/AsyncEntityPartTest.java | 32 +++-------------- 2 files changed, 9 insertions(+), 57 deletions(-) diff --git a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/EntityPartTest.java b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/EntityPartTest.java index bc35f735..de644e19 100644 --- a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/EntityPartTest.java +++ b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/EntityPartTest.java @@ -23,7 +23,6 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.net.URI; -import java.net.URISyntaxException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -55,6 +54,7 @@ import jakarta.ws.rs.core.EntityPart; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; /** * @author James R. Perkins @@ -188,33 +188,9 @@ public void uploadMultipleFiles() throws Exception { } private FileManagerClient createClient() { - try { - return RestClientBuilder.newBuilder() - .baseUri(createCombinedUri(uri, "entitypart")) - .build(FileManagerClient.class); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - private static URI createCombinedUri(final URI uri, final String path) throws URISyntaxException { - if (path == null || path.isEmpty()) { - return uri; - } - String uriString = uri.toString(); - final StringBuilder builder = new StringBuilder(uriString); - if (builder.charAt(builder.length() - 1) == '/') { - if (path.charAt(0) == '/') { - builder.append(path.substring(1)); - } else { - builder.append(path); - } - } else if (path.charAt(0) == '/') { - builder.append(path); - } else { - builder.append('/').append(path); - } - return new URI(builder.toString()); + return RestClientBuilder.newBuilder() + .baseUri(UriBuilder.fromUri(uri).path("entitypart").build()) + .build(FileManagerClient.class); } @Consumes(MediaType.MULTIPART_FORM_DATA) @@ -226,4 +202,4 @@ public interface FileManagerClient extends AutoCloseable { Response uploadFile(List entityParts) throws IOException; } -} \ No newline at end of file +} diff --git a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/asynctests/AsyncEntityPartTest.java b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/asynctests/AsyncEntityPartTest.java index bd495041..25ced1f1 100644 --- a/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/asynctests/AsyncEntityPartTest.java +++ b/tck/src/main/java/org/eclipse/microprofile/rest/client/tck/asynctests/AsyncEntityPartTest.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.net.URISyntaxException; import java.util.List; import java.util.concurrent.CompletionStage; @@ -51,6 +50,7 @@ import jakarta.ws.rs.core.EntityPart; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; /** * @author Neena Jacob @@ -135,33 +135,9 @@ public void uploadFileAsync() throws Exception { } private AsyncFileManagerClient createClient() { - try { - return RestClientBuilder.newBuilder() - .baseUri(createCombinedUri(uri, "entitypart")) - .build(AsyncFileManagerClient.class); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - private static URI createCombinedUri(final URI uri, final String path) throws URISyntaxException { - if (path == null || path.isEmpty()) { - return uri; - } - String uriString = uri.toString(); - final StringBuilder builder = new StringBuilder(uriString); - if (builder.charAt(builder.length() - 1) == '/') { - if (path.charAt(0) == '/') { - builder.append(path.substring(1)); - } else { - builder.append(path); - } - } else if (path.charAt(0) == '/') { - builder.append(path); - } else { - builder.append('/').append(path); - } - return new URI(builder.toString()); + return RestClientBuilder.newBuilder() + .baseUri(UriBuilder.fromUri(uri).path("entitypart").build()) + .build(AsyncFileManagerClient.class); } @Consumes(MediaType.MULTIPART_FORM_DATA)