From 357524af6ed9346441b5ed0bb4693d330be26944 Mon Sep 17 00:00:00 2001 From: jansupol Date: Thu, 12 Oct 2023 11:13:20 +0200 Subject: [PATCH] Support multipart by Jetty & Netty Signed-off-by: jansupol --- .../jetty/connector/JettyConnector.java | 27 +++- .../netty/connector/NettyConnector.java | 2 +- .../e2e/client/connector/MultiPartTest.java | 133 ++++++++++++++++++ 3 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java diff --git a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java index c8df6113048..9f54026733f 100644 --- a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java +++ b/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java @@ -26,6 +26,7 @@ import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -274,10 +275,13 @@ public CookieStore getCookieStore() { @Override public ClientResponse apply(final ClientRequest jerseyRequest) throws ProcessingException { final Request jettyRequest = translateRequest(jerseyRequest); - final Map clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest); - final ContentProvider entity = getBytesProvider(jerseyRequest); + final Map clientHeadersSnapshot = new HashMap<>(); + final ContentProvider entity = + getBytesProvider(jerseyRequest, jerseyRequest.getHeaders(), clientHeadersSnapshot, jettyRequest); if (entity != null) { jettyRequest.content(entity); + } else { + clientHeadersSnapshot.putAll(writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest)); } try { @@ -367,7 +371,10 @@ private Map writeOutBoundHeaders(final MultivaluedMap headers, + final Map snapshot, + final Request request) { final Object entity = clientRequest.getEntity(); if (entity == null) { @@ -378,6 +385,7 @@ private ContentProvider getBytesProvider(final ClientRequest clientRequest) { clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { @Override public OutputStream getOutputStream(final int contentLength) throws IOException { + snapshot.putAll(writeOutBoundHeaders(headers, request)); return outputStream; } }); @@ -390,7 +398,10 @@ public OutputStream getOutputStream(final int contentLength) throws IOException return new BytesContentProvider(outputStream.toByteArray()); } - private ContentProvider getStreamProvider(final ClientRequest clientRequest) { + private ContentProvider getStreamProvider(final ClientRequest clientRequest, + final MultivaluedMap headers, + final Map snapshot, + final Request request) { final Object entity = clientRequest.getEntity(); if (entity == null) { @@ -401,6 +412,7 @@ private ContentProvider getStreamProvider(final ClientRequest clientRequest) { clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { @Override public OutputStream getOutputStream(final int contentLength) throws IOException { + snapshot.putAll(writeOutBoundHeaders(headers, request)); return streamContentProvider.getOutputStream(); } }); @@ -421,10 +433,13 @@ private void processContent(final ClientRequest clientRequest, final ContentProv @Override public Future apply(final ClientRequest jerseyRequest, final AsyncConnectorCallback callback) { final Request jettyRequest = translateRequest(jerseyRequest); - final Map clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest); - final ContentProvider entity = getStreamProvider(jerseyRequest); + final Map clientHeadersSnapshot = new HashMap<>(); + final ContentProvider entity + = getStreamProvider(jerseyRequest, jerseyRequest.getHeaders(), clientHeadersSnapshot, jettyRequest); if (entity != null) { jettyRequest.content(entity); + } else { + clientHeadersSnapshot.putAll(writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest)); } final AtomicBoolean callbackInvoked = new AtomicBoolean(false); final Throwable failure; diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java index e98d982801e..d8971f5de18 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java @@ -437,6 +437,7 @@ public void operationComplete(io.netty.util.concurrent.Future futu jerseyRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { @Override public OutputStream getOutputStream(int contentLength) throws IOException { + replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes return entityWriter.getOutputStream(); } }); @@ -457,7 +458,6 @@ public void run() { jerseyRequest.writeEntity(); if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) { - replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes nettyRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, entityWriter.getLength()); entityWriter.flush(); } diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java new file mode 100644 index 00000000000..613925cd318 --- /dev/null +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.client.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.spi.ConnectorProvider; +import org.glassfish.jersey.jdk.connector.JdkConnectorProvider; +import org.glassfish.jersey.jetty.connector.JettyConnectorProvider; +import org.glassfish.jersey.netty.connector.NettyConnectorProvider; +import org.glassfish.jersey.logging.LoggingFeature; +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.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.spi.TestHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MultiPartTest { + + private static final Logger LOGGER = Logger.getLogger(RequestHeaderModificationsTest.class.getName()); + + public static List testData() { + return Arrays.asList( + new HttpUrlConnectorProvider(), + new JettyConnectorProvider(), + new NettyConnectorProvider(), + new JdkConnectorProvider() + ); + } + + @TestFactory + public Collection generateTests() { + Collection tests = new ArrayList<>(); + for (ConnectorProvider provider : testData()) { + HttpMultipartTest test = new HttpMultipartTest(provider) {}; + DynamicContainer container = TestHelper.toTestContainer(test, + String.format("MultiPartTest (%s)", provider.getClass().getSimpleName())); + tests.add(container); + } + return tests; + } + + public abstract static class HttpMultipartTest extends JerseyTest { + private final ConnectorProvider connectorProvider; + private static final String ENTITY = "hello"; + + public HttpMultipartTest(ConnectorProvider connectorProvider) { + this.connectorProvider = connectorProvider; + } + + @Override + protected Application configure() { + set(TestProperties.RECORD_LOG_LEVEL, Level.WARNING.intValue()); + enable(TestProperties.LOG_TRAFFIC); + return new ResourceConfig(MultipartResource.class) + .register(MultiPartFeature.class) + .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY)); + } + + @Override + protected void configureClient(ClientConfig clientConfig) { + clientConfig.connectorProvider(connectorProvider); + clientConfig.register(MultiPartFeature.class); + } + + @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(), + multiPart.getMediaType()); + } + } + + @Test + public void testMultipart() { + MultiPart multipart = new MultiPart().bodyPart(new BodyPart().entity(ENTITY)); + multipart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE); + + for (int i = 0; i != 5; i++) { + try (Response r = target().register(MultiPartFeature.class) + .path("upload") + .request() + .post(Entity.entity(multipart, multipart.getMediaType()))) { + Assertions.assertEquals(Response.Status.OK.getStatusCode(), r.getStatus()); + Assertions.assertEquals(ENTITY, r.readEntity(String.class)); + } + } + } + } +}