From c47b0fc4cfeaab733e3919bc9c56e0f4961be75f Mon Sep 17 00:00:00 2001 From: jansupol Date: Fri, 19 Jan 2024 15:40:44 +0100 Subject: [PATCH 1/2] Support Multipart with HelidonConnector/WebClient Signed-off-by: jansupol --- jersey/connector/pom.xml | 15 +++ .../jersey/connector/HelidonConnector.java | 5 +- .../jersey/connector/HelidonEntity.java | 35 ++++-- .../jersey/connector/MultipartTest.java | 103 ++++++++++++++++++ 4 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 jersey/connector/src/test/java/io/helidon/jersey/connector/MultipartTest.java diff --git a/jersey/connector/pom.xml b/jersey/connector/pom.xml index ec41f0ed8da..78536ef8118 100644 --- a/jersey/connector/pom.xml +++ b/jersey/connector/pom.xml @@ -50,6 +50,21 @@ io.helidon.webclient helidon-webclient + + org.glassfish.jersey.media + jersey-media-multipart + test + + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + + + io.helidon.microprofile.server + helidon-microprofile-server + test + io.helidon.config helidon-config-testing diff --git a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java index 8156474535a..5b9ed9df12f 100644 --- a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java +++ b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java @@ -60,7 +60,7 @@ class HelidonConnector implements Connector { private static final String HELIDON_VERSION = "Helidon/" + Version.VERSION + " (java " - + PropertiesHelper.getSystemProperty("java.runtime.version") + ")"; + + PropertiesHelper.getSystemProperty("java.runtime.version").run() + ")"; static final Logger LOGGER = Logger.getLogger(HelidonConnector.class.getName()); private final WebClient webClient; @@ -148,8 +148,6 @@ private CompletionStage applyInternal(ClientRequest request) { final WebClientRequestBuilder webClientRequestBuilder = webClient.method(request.getMethod()); webClientRequestBuilder.uri(request.getUri()); - webClientRequestBuilder.headers(HelidonStructures.createHeaders(request.getRequestHeaders())); - for (String propertyName : request.getConfiguration().getPropertyNames()) { Object property = request.getConfiguration().getProperty(propertyName); if (!propertyName.startsWith("jersey") && String.class.isInstance(property)) { @@ -177,6 +175,7 @@ private CompletionStage applyInternal(ClientRequest request) { entityType, request, webClientRequestBuilder, executorServiceKeeper.getExecutorService(request) ); } else { + webClientRequestBuilder.headers(HelidonStructures.createHeaders(request.getRequestHeaders())); responseStage = webClientRequestBuilder.submit(); } diff --git a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonEntity.java b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonEntity.java index fe38d292941..3eb5b8bf4dd 100644 --- a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonEntity.java +++ b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonEntity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutorService; import java.util.concurrent.Flow; @@ -106,27 +107,45 @@ static CompletionStage submit(HelidonEntityType type, if (type != null) { final int bufferSize = requestContext.resolveProperty( ClientProperties.OUTBOUND_CONTENT_LENGTH_BUFFER, 8192); + CompletableFuture firstWrite = new CompletableFuture<>(); switch (type) { case BYTE_ARRAY_OUTPUT_STREAM: final ByteArrayOutputStream baos = new ByteArrayOutputStream(bufferSize); - requestContext.setStreamProvider(contentLength -> baos); - ((ProcessingRunnable) requestContext::writeEntity).run(); - stage = requestBuilder.submit(baos); + requestContext.setStreamProvider(unused -> { + requestBuilder.headers(HelidonStructures.createHeaders(requestContext.getRequestHeaders())); + firstWrite.complete(null); + return baos; + }); + try { + requestContext.writeEntity(); + stage = requestBuilder.submit(baos); + } catch (IOException e) { + stage = CompletableFuture.failedStage(e); + } break; case READABLE_BYTE_CHANNEL: final OutputStreamChannel channel = new OutputStreamChannel(bufferSize); - requestContext.setStreamProvider(contentLength -> channel); + requestContext.setStreamProvider(unused -> { + requestBuilder.headers(HelidonStructures.createHeaders(requestContext.getRequestHeaders())); + firstWrite.complete(null); + return channel; + }); executorService.execute((ProcessingRunnable) requestContext::writeEntity); - stage = requestBuilder.submit(channel); + stage = firstWrite.thenCompose(unused -> requestBuilder.submit(channel)); break; case OUTPUT_STREAM_MULTI: final OutputStreamMulti publisher = IoMulti.outputStreamMulti(); - requestContext.setStreamProvider(contentLength -> publisher); + requestContext.setStreamProvider(unused -> { + requestBuilder.headers(HelidonStructures.createHeaders(requestContext.getRequestHeaders())); + firstWrite.complete(null); + return publisher; + }); executorService.execute((ProcessingRunnable) () -> { requestContext.writeEntity(); publisher.close(); }); - stage = requestBuilder.submit(Multi.create(publisher).map(DataChunk::create)); + Multi m = Multi.create(publisher).map(DataChunk::create); + stage = firstWrite.thenCompose(unused -> requestBuilder.submit(m)); break; default: } diff --git a/jersey/connector/src/test/java/io/helidon/jersey/connector/MultipartTest.java b/jersey/connector/src/test/java/io/helidon/jersey/connector/MultipartTest.java new file mode 100644 index 00000000000..ca4f5f21675 --- /dev/null +++ b/jersey/connector/src/test/java/io/helidon/jersey/connector/MultipartTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * 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 io.helidon.jersey.connector; + +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.AddExtension; +import io.helidon.microprofile.tests.junit5.DisableDiscovery; +import io.helidon.microprofile.tests.junit5.HelidonTest; +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.glassfish.jersey.media.multipart.BodyPart; +import org.glassfish.jersey.media.multipart.BodyPartEntity; +import org.glassfish.jersey.media.multipart.MultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.message.internal.ReaderWriter; +import org.junit.jupiter.api.Assertions; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +@HelidonTest +@DisableDiscovery +@AddExtension(ServerCdiExtension.class) +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(CdiComponentProvider.class) + +@AddBean(MultipartTest.MultipartApplication.class) +public class MultipartTest { + private static final String ENTITY = "hello"; + + public static class MultipartApplication extends Application { + @Override + public Set> getClasses() { + Set> set = new HashSet<>(); + set.add(MultiPartFeature.class); + set.add(MultipartResource.class); + return set; + } + } + + @Path("/") + public static class MultipartResource { + @POST + @Path("upload") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String upload(@Context HttpHeaders headers, MultiPart multiPart) throws IOException { + return ReaderWriter.readFromAsString( + ((BodyPartEntity) multiPart.getBodyParts().get(0).getEntity()).getInputStream(), + MediaType.TEXT_PLAIN_TYPE); + } + } + + @ParameterizedTest + @EnumSource(value = HelidonEntity.HelidonEntityType.class) + void testMultipart(HelidonEntity.HelidonEntityType entityType, WebTarget webTarget) { + // For each entity type make 10 consecutive requests + for (int i = 0; i != 10; i++) { + MultiPart multipart = new MultiPart().bodyPart(new BodyPart().entity(ENTITY)); + multipart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE); + try (Response r = ClientBuilder.newBuilder() + .property(HelidonConnector.INTERNAL_ENTITY_TYPE, entityType.name()) + .build().target(webTarget.getUri()) + .path("upload") + .register(MultiPartFeature.class) + .request() + .post(Entity.entity(multipart, multipart.getMediaType()))) { + Assertions.assertEquals(Response.Status.OK.getStatusCode(), r.getStatus()); + Assertions.assertEquals(ENTITY, r.readEntity(String.class)); + } + } + } +} \ No newline at end of file From 1cab30fa9436547a4814dbf85f96e9d4b02cabb7 Mon Sep 17 00:00:00 2001 From: Romain Grecourt Date: Wed, 21 Feb 2024 14:51:11 -0800 Subject: [PATCH 2/2] Use hamcrest instead of junit Assertions --- .../java/io/helidon/jersey/connector/MultipartTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/jersey/connector/src/test/java/io/helidon/jersey/connector/MultipartTest.java b/jersey/connector/src/test/java/io/helidon/jersey/connector/MultipartTest.java index ca4f5f21675..198eb3dc946 100644 --- a/jersey/connector/src/test/java/io/helidon/jersey/connector/MultipartTest.java +++ b/jersey/connector/src/test/java/io/helidon/jersey/connector/MultipartTest.java @@ -28,7 +28,6 @@ import org.glassfish.jersey.media.multipart.MultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.message.internal.ReaderWriter; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -49,6 +48,9 @@ import java.util.HashSet; import java.util.Set; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + @HelidonTest @DisableDiscovery @AddExtension(ServerCdiExtension.class) @@ -95,8 +97,8 @@ void testMultipart(HelidonEntity.HelidonEntityType entityType, WebTarget webTarg .register(MultiPartFeature.class) .request() .post(Entity.entity(multipart, multipart.getMediaType()))) { - Assertions.assertEquals(Response.Status.OK.getStatusCode(), r.getStatus()); - Assertions.assertEquals(ENTITY, r.readEntity(String.class)); + assertThat(r.getStatus(), is(Response.Status.OK.getStatusCode())); + assertThat(r.readEntity(String.class), is(ENTITY)); } } }