() {
+ @Override
+ public void completed(Response response) {
+ try {
+ final String result = response.readEntity(String.class);
+ results.put(id, result);
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void failed(Throwable error) {
+ Logger.getLogger(HelloWorldTest.class.getName()).log(Level.SEVERE, "Failed on throwable", error);
+ results.put(id, "error: " + error.getMessage());
+ latch.countDown();
+ }
+ });
+ }
+ assertTrue(latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS));
+ final long toc = System.currentTimeMillis();
+ Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic));
+
+ StringBuilder resultInfo = new StringBuilder("Results:\n");
+ for (int i = 0; i < REQUESTS; i++) {
+ String result = results.get(i);
+ resultInfo.append(i).append(": ").append(result).append('\n');
+ }
+ Logger.getLogger(HelloWorldTest.class.getName()).info(resultInfo.toString());
+
+ for (int i = 0; i < REQUESTS; i++) {
+ String result = results.get(i);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, result);
+ }
+ }
+
+ @Test
+ public void testHead() {
+ Response response = target().path(ROOT_PATH).request().head();
+ assertEquals(200, response.getStatus());
+ assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+ }
+
+ @Test
+ public void testFooBarOptions() {
+ Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals("foo/bar", response.getMediaType().toString());
+ assertEquals(0, response.getLength());
+ }
+
+ @Test
+ public void testTextPlainOptions() {
+ Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+ final String responseBody = response.readEntity(String.class);
+ _checkAllowContent(responseBody);
+ }
+
+ private void _checkAllowContent(final String content) {
+ assertTrue(content.contains("GET"));
+ assertTrue(content.contains("HEAD"));
+ assertTrue(content.contains("OPTIONS"));
+ }
+
+ @Test
+ public void testMissingResourceNotFound() {
+ Response response;
+
+ response = target().path(ROOT_PATH + "arbitrary").request().get();
+ assertEquals(404, response.getStatus());
+ response.close();
+
+ response = target().path(ROOT_PATH).path("arbitrary").request().get();
+ assertEquals(404, response.getStatus());
+ response.close();
+ }
+
+ @Test
+ public void testLoggingFilterClientClass() {
+ Client client = client();
+ client.register(CustomLoggingFilter.class).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testLoggingFilterClientInstance() {
+ Client client = client();
+ client.register(new CustomLoggingFilter()).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testLoggingFilterTargetClass() {
+ WebTarget target = target().path(ROOT_PATH);
+ target.register(CustomLoggingFilter.class).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target.request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testLoggingFilterTargetInstance() {
+ WebTarget target = target().path(ROOT_PATH);
+ target.register(new CustomLoggingFilter()).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target.request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testConfigurationUpdate() {
+ Client client1 = client();
+ client1.register(CustomLoggingFilter.class).property("foo", "bar");
+
+ Client client = ClientBuilder.newClient(client1.getConfiguration());
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = client.target(getBaseUri()).path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ /**
+ * JERSEY-2157 reproducer.
+ *
+ * The test ensures that entities of the error responses which cause
+ * WebApplicationException being thrown by a JAX-RS client are buffered
+ * and that the underlying input connections are automatically released
+ * in such case.
+ */
+ @Test
+ public void testConnectionClosingOnExceptionsForErrorResponses() {
+ final BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager();
+ final AtomicInteger connectionCounter = new AtomicInteger(0);
+
+ final ClientConfig config = new ClientConfig().property(Apache5ClientProperties.CONNECTION_MANAGER,
+ new HttpClientConnectionManager() {
+ @Override
+ public LeaseRequest lease(String id, HttpRoute route, Timeout requestTimeout, Object state) {
+ connectionCounter.incrementAndGet();
+ return cm.lease(id, route, requestTimeout, state);
+ }
+
+ @Override
+ public void release(ConnectionEndpoint endpoint, Object newState, TimeValue validDuration) {
+ connectionCounter.decrementAndGet();
+ cm.release(endpoint, newState, validDuration);
+ }
+
+ @Override
+ public void connect(
+ ConnectionEndpoint endpoint,
+ TimeValue connectTimeout,
+ HttpContext context
+ ) throws IOException {
+ cm.connect(endpoint, connectTimeout, context);
+ }
+
+ @Override
+ public void upgrade(ConnectionEndpoint endpoint, HttpContext context) throws IOException {
+ cm.upgrade(endpoint, context);
+ }
+
+ @Override
+ public void close(CloseMode closeMode) {
+ cm.close(closeMode);
+ }
+
+ @Override
+ public void close() throws IOException {
+ cm.close();
+ }
+ });
+ config.connectorProvider(new Apache5ConnectorProvider());
+
+ final Client client = ClientBuilder.newClient(config);
+ final WebTarget rootTarget = client.target(getBaseUri()).path(ROOT_PATH);
+
+ // Test that connection is getting closed properly for error responses.
+ try {
+ final String response = rootTarget.path("error").request().get(String.class);
+ fail("Exception expected. Received: " + response);
+ } catch (InternalServerErrorException isee) {
+ // do nothing - connection should be closed properly by now
+ }
+
+ // Fail if the previous connection has not been closed automatically.
+ assertEquals(0, connectionCounter.get());
+
+ try {
+ final String response = rootTarget.path("error2").request().get(String.class);
+ fail("Exception expected. Received: " + response);
+ } catch (InternalServerErrorException isee) {
+ assertEquals("Received unexpected data.", "Error2.", isee.getResponse().readEntity(String.class));
+ // Test buffering:
+ // second read would fail if entity was not buffered
+ assertEquals("Unexpected data in the entity buffer.", "Error2.", isee.getResponse().readEntity(String.class));
+ }
+
+ assertEquals(0, connectionCounter.get());
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java
new file mode 100644
index 0000000000..cad5ea8494
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
+import org.apache.hc.core5.http.io.entity.InputStreamEntity;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Assert;
+import org.junit.Test;
+
+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.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.ByteArrayInputStream;
+import java.util.logging.Logger;
+
+public class HttpEntityTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HttpEntityTest.class.getName());
+ private static final String ECHO_MESSAGE = "ECHO MESSAGE";
+
+ @Path("/")
+ public static class Resource {
+ @POST
+ public String echo(String message) {
+ return message;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(Resource.class)
+ .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ @Test
+ public void testInputStreamEntity() {
+ ByteArrayInputStream bais = new ByteArrayInputStream(ECHO_MESSAGE.getBytes());
+ InputStreamEntity entity = new InputStreamEntity(bais, ContentType.TEXT_PLAIN);
+
+ try (Response response = target().request().post(Entity.entity(entity, MediaType.APPLICATION_OCTET_STREAM))) {
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(ECHO_MESSAGE, response.readEntity(String.class));
+ }
+ }
+
+ @Test
+ public void testByteArrayEntity() {
+ ByteArrayEntity entity = new ByteArrayEntity(ECHO_MESSAGE.getBytes(), ContentType.TEXT_PLAIN);
+
+ try (Response response = target().request().post(Entity.entity(entity, MediaType.APPLICATION_OCTET_STREAM))) {
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(ECHO_MESSAGE, response.readEntity(String.class));
+ }
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java
new file mode 100644
index 0000000000..c63b7a87ee
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.logging.Logger;
+
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class HttpHeadersTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+
+ @POST
+ public String post(
+ @HeaderParam("Transfer-Encoding") String transferEncoding,
+ @HeaderParam("X-CLIENT") String xClient,
+ @HeaderParam("X-WRITER") String xWriter,
+ String entity) {
+ assertEquals("client", xClient);
+ if (transferEncoding == null || !transferEncoding.equals("chunked")) {
+ assertEquals("writer", xWriter);
+ }
+ return entity;
+ }
+ }
+
+ @Provider
+ @Produces("text/plain")
+ public static class HeaderWriter implements MessageBodyWriter {
+
+ public boolean isWriteable(Class> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ return type == String.class;
+ }
+
+ public long getSize(String t, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ return -1;
+ }
+
+ public void writeTo(String t,
+ Class> type,
+ Type genericType,
+ Annotation[] annotations,
+ MediaType mediaType,
+ MultivaluedMap httpHeaders,
+ OutputStream entityStream) throws IOException, WebApplicationException {
+ httpHeaders.add("X-WRITER", "writer");
+ entityStream.write(t.getBytes());
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ enable(TestProperties.LOG_TRAFFIC);
+ enable(TestProperties.DUMP_ENTITY);
+
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class, HeaderWriter.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.property(ClientProperties.READ_TIMEOUT, 1000).connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = target("test");
+
+ Response cr = r.request().header("X-CLIENT", "client").post(Entity.text("POST"));
+ assertEquals(200, cr.getStatus());
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPostChunked() {
+ WebTarget r = target("test");
+
+ Response cr = r.request().header("X-CLIENT", "client").post(Entity.text("POST"));
+ assertEquals(200, cr.getStatus());
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java
new file mode 100644
index 0000000000..00a45a15b6
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.ws.rs.ClientErrorException;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class HttpMethodTest extends JerseyTest {
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(HttpMethodResource.class, ErrorResource.class);
+ }
+
+ protected Client createClient() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ return ClientBuilder.newClient(cc);
+ }
+
+ protected Client createPoolingClient() {
+ ClientConfig cc = new ClientConfig();
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+ connectionManager.setMaxTotal(100);
+ connectionManager.setDefaultMaxPerRoute(100);
+ cc.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ return ClientBuilder.newClient(cc);
+ }
+
+ private WebTarget getWebTarget(final Client client) {
+ return client.target(getBaseUri()).path("test");
+ }
+
+ private WebTarget getWebTarget() {
+ return getWebTarget(createClient());
+ }
+
+ @Target({ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ @HttpMethod("PATCH")
+ public @interface PATCH {
+ }
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @POST
+ public String post(String entity) {
+ return entity;
+ }
+
+ @PUT
+ public String put(String entity) {
+ return entity;
+ }
+
+ @DELETE
+ public String delete() {
+ return "DELETE";
+ }
+
+ @DELETE
+ @Path("withentity")
+ public String delete(String entity) {
+ return entity;
+ }
+
+ @POST
+ @Path("noproduce")
+ public void postNoProduce(String entity) {
+ }
+
+ @POST
+ @Path("noconsumeproduce")
+ public void postNoConsumeProduce() {
+ }
+
+ @PATCH
+ public String patch(String entity) {
+ return entity;
+ }
+ }
+
+ @Test
+ public void testHead() {
+ WebTarget r = getWebTarget();
+ Response cr = r.request().head();
+ assertFalse(cr.hasEntity());
+ }
+
+ @Test
+ public void testOptions() {
+ WebTarget r = getWebTarget();
+ Response cr = r.request().options();
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testOptionsWithEntity() {
+ WebTarget r = getWebTarget();
+ Response response = r.request().build("OPTIONS", Entity.text("OPTIONS")).invoke();
+ assertEquals(200, response.getStatus());
+ response.close();
+ }
+
+ @Test
+ public void testGet() {
+ WebTarget r = getWebTarget();
+ assertEquals("GET", r.request().get(String.class));
+
+ Response cr = r.request().get();
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = getWebTarget();
+ assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+
+ Response cr = r.request().post(Entity.text("POST"));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPostChunked() {
+ ClientConfig cc = new ClientConfig()
+ .property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024)
+ .connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = getWebTarget(client);
+
+ assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+
+ Response cr = r.request().post(Entity.text("POST"));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPostVoid() {
+ WebTarget r = getWebTarget(createPoolingClient());
+
+ for (int i = 0; i < 100; i++) {
+ r.request().post(Entity.text("POST"));
+ }
+ }
+
+ @Test
+ public void testPostNoProduce() {
+ WebTarget r = getWebTarget();
+ assertEquals(204, r.path("noproduce").request().post(Entity.text("POST")).getStatus());
+
+ Response cr = r.path("noproduce").request().post(Entity.text("POST"));
+ assertFalse(cr.hasEntity());
+ cr.close();
+ }
+
+
+ @Test
+ public void testPostNoConsumeProduce() {
+ WebTarget r = getWebTarget();
+ assertEquals(204, r.path("noconsumeproduce").request().post(null).getStatus());
+
+ Response cr = r.path("noconsumeproduce").request().post(Entity.text("POST"));
+ assertFalse(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPut() {
+ WebTarget r = getWebTarget();
+ assertEquals("PUT", r.request().put(Entity.text("PUT"), String.class));
+
+ Response cr = r.request().put(Entity.text("PUT"));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testDelete() {
+ WebTarget r = getWebTarget();
+ assertEquals("DELETE", r.request().delete(String.class));
+
+ Response cr = r.request().delete();
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPatch() {
+ WebTarget r = getWebTarget();
+ assertEquals("PATCH", r.request().method("PATCH", Entity.text("PATCH"), String.class));
+
+ Response cr = r.request().method("PATCH", Entity.text("PATCH"));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testAll() {
+ WebTarget r = getWebTarget();
+
+ assertEquals("GET", r.request().get(String.class));
+
+ assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+
+ assertEquals(204, r.path("noproduce").request().post(Entity.text("POST")).getStatus());
+
+ assertEquals(204, r.path("noconsumeproduce").request().post(null).getStatus());
+
+ assertEquals("PUT", r.request().post(Entity.text("PUT"), String.class));
+
+ assertEquals("DELETE", r.request().delete(String.class));
+ }
+
+
+ @Path("/error")
+ public static class ErrorResource {
+ @POST
+ public Response post(String entity) {
+ return Response.serverError().build();
+ }
+
+ @Path("entity")
+ @POST
+ public Response postWithEntity(String entity) {
+ return Response.serverError().entity("error").build();
+ }
+ }
+
+ @Test
+ public void testPostError() {
+ WebTarget r = createClient().target(getBaseUri()).path("error");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ final Response post = r.request().post(Entity.text("POST"));
+ post.close();
+ } catch (ClientErrorException ex) {
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorWithEntity() {
+ WebTarget r = createPoolingClient().target(getBaseUri()).path("error/entity");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ String s = ex.getResponse().readEntity(String.class);
+ assertEquals("error", s);
+ }
+ }
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java
new file mode 100644
index 0000000000..35b38c3bdc
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class HttpMethodWithClientFilterTest extends HttpMethodTest {
+
+ @Override
+ protected Client createClient() {
+ ClientConfig cc = new ClientConfig()
+ .register(LoggingFeature.class)
+ .connectorProvider(new Apache5ConnectorProvider());
+ return ClientBuilder.newClient(cc);
+ }
+
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java
new file mode 100644
index 0000000000..40f1b4136d
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.logging.Logger;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.ServerErrorException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.StreamingOutput;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * The LargeDataTest reproduces a problem when bytes of large data sent are incorrectly sent.
+ * As a result, the request body is different than what was sent by the client.
+ *
+ * In order to be able to inspect the request body, the generated data is a sequence of numbers
+ * delimited with new lines. Such as
+ *
+ * 1
+ * 2
+ * 3
+ *
+ * ...
+ *
+ * 57234
+ * 57235
+ * 57236
+ *
+ * ...
+ *
+ * It is also possible to send the data to netcat: {@code nc -l 8080} and verify the problem is
+ * on the client side.
+ *
+ * @author Stepan Vavra
+ * @author Marek Potociar
+ */
+public class LargeDataTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(LargeDataTest.class.getName());
+ private static final int LONG_DATA_SIZE = 1_000_000; // for large set around 5GB, try e.g.: 536_870_912;
+ private static volatile Throwable exception;
+
+ private static StreamingOutput longData(long sequence) {
+ return out -> {
+ long offset = 0;
+ while (offset < sequence) {
+ out.write(Long.toString(offset).getBytes());
+ out.write('\n');
+ offset++;
+ }
+ };
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ @Test
+ public void postWithLargeData() throws Throwable {
+ WebTarget webTarget = target("test");
+
+ Response response = webTarget.request().post(Entity.entity(longData(LONG_DATA_SIZE), MediaType.TEXT_PLAIN_TYPE));
+
+ try {
+ if (exception != null) {
+
+ // the reason to throw the exception is that IntelliJ gives you an option to compare the expected with the actual
+ throw exception;
+ }
+
+ Assert.assertEquals("Unexpected error: " + response.getStatus(),
+ Status.Family.SUCCESSFUL,
+ response.getStatusInfo().getFamily());
+ } finally {
+ response.close();
+ }
+ }
+
+ @Path("/test")
+ public static class HttpMethodResource {
+
+ @POST
+ public Response post(InputStream content) {
+ try {
+
+ longData(LONG_DATA_SIZE).write(new OutputStream() {
+
+ private long position = 0;
+// private long mbRead = 0;
+
+ @Override
+ public void write(final int generated) throws IOException {
+ int received = content.read();
+
+ if (received != generated) {
+ throw new IOException("Bytes don't match at position " + position
+ + ": received=" + received
+ + ", generated=" + generated);
+ }
+
+ position++;
+// if (position % (1024 * 1024) == 0) {
+// mbRead++;
+// System.out.println("MB read: " + mbRead);
+// }
+ }
+ });
+ } catch (IOException e) {
+ exception = e;
+ throw new ServerErrorException(e.getMessage(), 500, e);
+ }
+
+ return Response.ok().build();
+ }
+
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java
new file mode 100644
index 0000000000..46ba4ee018
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.DynamicFeature;
+import javax.ws.rs.container.ResourceInfo;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.FeatureContext;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ClientBinding;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.Uri;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Jersey programmatic managed client test
+ *
+ * @author Marek Potociar
+ */
+public class ManagedClientTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName());
+
+ /**
+ * Managed client configuration for client A.
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+ @ClientBinding(configClass = MyClientAConfig.class)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.PARAMETER})
+ public static @interface ClientA {
+ }
+
+ /**
+ * Managed client configuration for client B.
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+ @ClientBinding(configClass = MyClientBConfig.class)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.PARAMETER})
+ public @interface ClientB {
+ }
+
+ /**
+ * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance
+ * to every method that is annotated with {@link Require @Require} internal feature
+ * annotation.
+ *
+ * @author Marek Potociar
+ */
+ public static class CustomHeaderFeature implements DynamicFeature {
+
+ /**
+ * A method annotation to be placed on those resource methods to which a validating
+ * {@link CustomHeaderFilter} instance should be added.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ @Target(ElementType.METHOD)
+ public static @interface Require {
+
+ /**
+ * Expected custom header name to be validated by the {@link CustomHeaderFilter}.
+ */
+ public String headerName();
+
+ /**
+ * Expected custom header value to be validated by the {@link CustomHeaderFilter}.
+ */
+ public String headerValue();
+ }
+
+ @Override
+ public void configure(ResourceInfo resourceInfo, FeatureContext context) {
+ final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class);
+ if (va != null) {
+ context.register(new CustomHeaderFilter(va.headerName(), va.headerValue()));
+ }
+ }
+ }
+
+ /**
+ * A filter for appending and validating custom headers.
+ *
+ * On the client side, appends a new custom request header with a configured name and value to each outgoing request.
+ *
+ *
+ * On the server side, validates that each request has a custom header with a configured name and value.
+ * If the validation fails a HTTP 403 response is returned.
+ *
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+ public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter {
+
+ private final String headerName;
+ private final String headerValue;
+
+ public CustomHeaderFilter(String headerName, String headerValue) {
+ if (headerName == null || headerValue == null) {
+ throw new IllegalArgumentException("Header name and value must not be null.");
+ }
+ this.headerName = headerName;
+ this.headerValue = headerValue;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext ctx) throws IOException { // validate
+ if (!headerValue.equals(ctx.getHeaderString(headerName))) {
+ ctx.abortWith(Response.status(Response.Status.FORBIDDEN)
+ .type(MediaType.TEXT_PLAIN)
+ .entity(String
+ .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue))
+ .build());
+ }
+ }
+
+ @Override
+ public void filter(ClientRequestContext ctx) throws IOException { // append
+ ctx.getHeaders().putSingle(headerName, headerValue);
+ }
+ }
+
+ /**
+ * Internal resource accessed from the managed client resource.
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+ @Path("internal")
+ public static class InternalResource {
+
+ @GET
+ @Path("a")
+ @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a")
+ public String getA() {
+ return "a";
+ }
+
+ @GET
+ @Path("b")
+ @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b")
+ public String getB() {
+ return "b";
+ }
+ }
+
+ /**
+ * A resource that uses managed clients to retrieve values of internal
+ * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter}
+ * and require a specific custom header in a request to be set to a specific value.
+ *
+ * Properly configured managed clients have a {@code CustomHeaderFilter} instance
+ * configured to insert the {@link CustomHeaderFeature.Require required} custom header
+ * with a proper value into the outgoing client requests.
+ *
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+ @Path("public")
+ public static class PublicResource {
+
+ @Uri("a")
+ @ClientA // resolves to /internal/a
+ private WebTarget targetA;
+
+ @GET
+ @Produces("text/plain")
+ @Path("a")
+ public String getTargetA() {
+ return targetA.request(MediaType.TEXT_PLAIN).get(String.class);
+ }
+
+ @GET
+ @Produces("text/plain")
+ @Path("b")
+ public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) {
+ return targetB.request(MediaType.TEXT_PLAIN).get();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class)
+ .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal");
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ public static class MyClientAConfig extends ClientConfig {
+
+ public MyClientAConfig() {
+ this.register(new CustomHeaderFilter("custom-header", "a"));
+ }
+ }
+
+ public static class MyClientBConfig extends ClientConfig {
+
+ public MyClientBConfig() {
+ this.register(new CustomHeaderFilter("custom-header", "b"));
+ }
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ /**
+ * Test that a connection via managed clients works properly.
+ *
+ * @throws Exception in case of test failure.
+ */
+ @Test
+ public void testManagedClient() throws Exception {
+ final WebTarget resource = target().path("public").path("{name}");
+ Response response;
+
+ response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get();
+ assertEquals(200, response.getStatus());
+ assertEquals("a", response.readEntity(String.class));
+
+ response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get();
+ assertEquals(200, response.getStatus());
+ assertEquals("b", response.readEntity(String.class));
+ }
+
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java
new file mode 100644
index 0000000000..cdea49be4c
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class NoEntityTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @GET
+ public Response get() {
+ return Response.status(Status.CONFLICT).build();
+ }
+
+ @POST
+ public void post(String entity) {
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ @Test
+ public void testGet() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().get();
+ cr.close();
+ }
+ }
+
+ @Test
+ public void testGetWithClose() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().get();
+ cr.close();
+ }
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().post(null);
+ }
+ }
+
+ @Test
+ public void testPostWithClose() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().post(null);
+ cr.close();
+ }
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java
new file mode 100644
index 0000000000..6254c430fc
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.io.IOException;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.TimeValue;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+public class RetryStrategyTest extends JerseyTest {
+ private static final int READ_TIMEOUT_MS = 100;
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(RetryHandlerResource.class);
+ }
+
+ @Path("/")
+ public static class RetryHandlerResource {
+ private static volatile int postRequestNumber = 0;
+ private static volatile int getRequestNumber = 0;
+
+ // Cause a timeout on the first GET and POST request
+ @GET
+ public String get(@Context HttpHeaders h) {
+ if (getRequestNumber++ == 0) {
+ try {
+ Thread.sleep(READ_TIMEOUT_MS * 10);
+ } catch (InterruptedException ex) {
+ // ignore
+ }
+ }
+ return "GET";
+ }
+
+ @POST
+ public String post(@Context HttpHeaders h, String e) {
+ if (postRequestNumber++ == 0) {
+ try {
+ Thread.sleep(READ_TIMEOUT_MS * 10);
+ } catch (InterruptedException ex) {
+ // ignore
+ }
+ }
+ return "POST";
+ }
+ }
+
+ @Test
+ public void testRetryGet() throws IOException {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ cc.property(Apache5ClientProperties.RETRY_STRATEGY,
+ new HttpRequestRetryStrategy() {
+ @Override
+ public boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context) {
+ return true;
+ }
+
+ @Override
+ public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) {
+ return true;
+ }
+
+ @Override
+ public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) {
+ return TimeValue.ofMilliseconds(200);
+ }
+ });
+ cc.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS);
+ Client client = ClientBuilder.newClient(cc);
+
+ WebTarget r = client.target(getBaseUri());
+ assertEquals("GET", r.request().get(String.class));
+ }
+
+ @Test
+ public void testRetryPost() throws IOException {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ cc.property(Apache5ClientProperties.RETRY_STRATEGY,
+ new HttpRequestRetryStrategy() {
+ @Override
+ public boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context) {
+ return true;
+ }
+
+ @Override
+ public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) {
+ return true;
+ }
+
+ @Override
+ public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) {
+ return TimeValue.ofMilliseconds(200);
+ }
+ });
+ cc.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS);
+ Client client = ClientBuilder.newClient(cc);
+
+ WebTarget r = client.target(getBaseUri());
+ assertEquals("POST", r.request()
+ .property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED)
+ .post(Entity.text("POST"), String.class));
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java
new file mode 100644
index 0000000000..ce6a377883
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ *
+ * @author Miroslav Fuksa
+ */
+public class SpecialHeaderTest extends JerseyTest {
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(MyResource.class, GZipEncoder.class, LoggingFeature.class);
+ }
+
+ @Path("resource")
+ public static class MyResource {
+ @GET
+ @Produces("text/plain")
+ @Path("encoded")
+ public Response getEncoded() {
+ return Response.ok("get").header(HttpHeaders.CONTENT_ENCODING, "gzip").build();
+ }
+
+ @GET
+ @Produces("text/plain")
+ @Path("non-encoded")
+ public Response getNormal() {
+ return Response.ok("get").build();
+ }
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+
+ @Test
+ @Ignore("Apache connector does not provide information about encoding for gzip and deflate encoding")
+ public void testEncoded() {
+ final Response response = target().path("resource/encoded").request("text/plain").get();
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals("get", response.readEntity(String.class));
+ Assert.assertEquals("gzip", response.getHeaderString(HttpHeaders.CONTENT_ENCODING));
+ Assert.assertEquals("text/plain", response.getHeaderString(HttpHeaders.CONTENT_TYPE));
+ Assert.assertEquals(3, response.getHeaderString(HttpHeaders.CONTENT_LENGTH));
+ }
+
+ @Test
+ public void testNonEncoded() {
+ final Response response = target().path("resource/non-encoded").request("text/plain").get();
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals("get", response.readEntity(String.class));
+ Assert.assertNull(response.getHeaderString(HttpHeaders.CONTENT_ENCODING));
+ Assert.assertEquals("text/plain", response.getHeaderString(HttpHeaders.CONTENT_TYPE));
+ Assert.assertEquals("3", response.getHeaderString(HttpHeaders.CONTENT_LENGTH));
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java
new file mode 100644
index 0000000000..ea90e79879
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.inject.Singleton;
+
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.server.ChunkedOutput;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Petr Janouch
+ */
+public class StreamingTest extends JerseyTest {
+ private PoolingHttpClientConnectionManager connectionManager;
+
+ /**
+ * Test that a data stream can be terminated from the client side.
+ */
+ @Test
+ public void clientCloseNoTimeoutTest() throws IOException {
+ clientCloseTest(-1);
+ }
+
+ @Test
+ public void clientCloseWithTimeOutTest() throws IOException {
+ clientCloseTest(1_000);
+ }
+
+ /**
+ * Tests that closing a response after completely reading the entity reuses the connection
+ */
+ @Test
+ public void reuseConnectionTest() throws IOException {
+ Response response = target().path("/streamingEndpoint/get").request().get();
+ InputStream is = response.readEntity(InputStream.class);
+ byte[] buf = new byte[8192];
+ is.read(buf);
+ is.close();
+ response.close();
+
+ assertEquals(1, connectionManager.getTotalStats().getAvailable());
+ assertEquals(0, connectionManager.getTotalStats().getLeased());
+ }
+
+ /**
+ * Tests that closing a request without reading the entity does not throw an exception.
+ */
+ @Test
+ public void clientCloseThrowsNoExceptionTest() throws IOException {
+ Response response = target().path("/streamingEndpoint/get").request().get();
+ response.close();
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ connectionManager = new PoolingHttpClientConnectionManager();
+ config.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager);
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(StreamingEndpoint.class);
+ }
+
+ /**
+ * Test that a data stream can be terminated from the client side.
+ */
+ private void clientCloseTest(int readTimeout) throws IOException {
+ // start streaming
+ AtomicInteger counter = new AtomicInteger(0);
+ Invocation.Builder builder = target().path("/streamingEndpoint").request();
+ if (readTimeout > -1) {
+ counter.set(1);
+ builder.property(ClientProperties.READ_TIMEOUT, readTimeout);
+ builder.property(Apache5ClientProperties.CONNECTION_CLOSING_STRATEGY,
+ (Apache5ConnectionClosingStrategy) (config, request, response, stream) -> {
+ try {
+ stream.close();
+ } catch (Exception e) {
+ // timeout, no chunk ending
+ } finally {
+ counter.set(0);
+ response.close();
+ }
+ });
+ }
+ InputStream inputStream = builder.get(InputStream.class);
+
+ WebTarget sendTarget = target().path("/streamingEndpoint/send");
+ // trigger sending 'A' to the stream; OK is sent if everything on the server was OK
+ assertEquals("OK", sendTarget.request().get().readEntity(String.class));
+ // check 'A' has been sent
+ assertEquals('A', inputStream.read());
+ // closing the stream should tear down the connection
+ inputStream.close();
+ // trigger sending another 'A' to the stream; it should fail
+ // (indicating that the streaming has been terminated on the server)
+ assertEquals("NOK", sendTarget.request().get().readEntity(String.class));
+ assertEquals(0, counter.get());
+ }
+
+ @Singleton
+ @Path("streamingEndpoint")
+ public static class StreamingEndpoint {
+
+ private final ChunkedOutput output = new ChunkedOutput<>(String.class);
+
+ @GET
+ @Path("send")
+ public String sendEvent() {
+ try {
+ output.write("A");
+ } catch (IOException e) {
+ return "NOK";
+ }
+
+ return "OK";
+ }
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public ChunkedOutput get() {
+ return output;
+ }
+
+ @GET
+ @Path("get")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String getString() {
+ return "OK";
+ }
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java
new file mode 100644
index 0000000000..7eab1b30b6
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.net.SocketTimeoutException;
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Martin Matula
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class TimeoutTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName());
+
+ @Path("/test")
+ public static class TimeoutResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("timeout")
+ public String getTimeout() {
+ try {
+ Thread.sleep(2000);
+ } catch (final InterruptedException e) {
+ e.printStackTrace();
+ }
+ return "GET";
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ final ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(final ClientConfig config) {
+ config.property(ClientProperties.READ_TIMEOUT, 1000);
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ @Test
+ public void testFast() {
+ final Response r = target("test").request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testSlow() {
+ try {
+ target("test/timeout").request().get();
+ fail("Timeout expected.");
+ } catch (final ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(SocketTimeoutException.class));
+ }
+ }
+
+ @Test
+ public void testPerRequestTimeout() {
+ final Response r = target("test/timeout").request()
+ .property(ClientProperties.READ_TIMEOUT, 3000).get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java
new file mode 100644
index 0000000000..d95c2f21c8
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.process.Inflector;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * This very basic resource showcases support of a HTTP TRACE method,
+ * not directly supported by JAX-RS API.
+ *
+ * @author Marek Potociar
+ */
+public class TraceSupportTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName());
+
+ /**
+ * Programmatic tracing root resource path.
+ */
+ public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic";
+
+ /**
+ * Annotated class-based tracing root resource path.
+ */
+ public static final String ROOT_PATH_ANNOTATED = "tracing/annotated";
+
+ @HttpMethod(TRACE.NAME)
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TRACE {
+ public static final String NAME = "TRACE";
+ }
+
+ @Path(ROOT_PATH_ANNOTATED)
+ public static class TracingResource {
+
+ @TRACE
+ @Produces("text/plain")
+ public String trace(Request request) {
+ return stringify((ContainerRequest) request);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TracingResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC);
+ resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector() {
+
+ @Override
+ public Response apply(ContainerRequestContext request) {
+ if (request == null) {
+ return Response.noContent().build();
+ } else {
+ return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build();
+ }
+ }
+ });
+
+ return config.registerResources(resourceBuilder.build());
+
+ }
+
+ private String[] expectedFragmentsProgrammatic = new String[]{
+ "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic"
+ };
+ private String[] expectedFragmentsAnnotated = new String[]{
+ "TRACE http://localhost:" + this.getPort() + "/tracing/annotated"
+ };
+
+ private WebTarget prepareTarget(String path) {
+ final WebTarget target = target();
+ target.register(LoggingFeature.class);
+ return target.path(path);
+ }
+
+ @Test
+ public void testProgrammaticApp() throws Exception {
+ Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+ String responseEntity = response.readEntity(String.class);
+ for (String expectedFragment : expectedFragmentsProgrammatic) {
+ assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity,
+ // toLowerCase - http header field names are case insensitive
+ responseEntity.contains(expectedFragment));
+ }
+ }
+
+ @Test
+ public void testAnnotatedApp() throws Exception {
+ Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+ String responseEntity = response.readEntity(String.class);
+ for (String expectedFragment : expectedFragmentsAnnotated) {
+ assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity,
+ // toLowerCase - http header field names are case insensitive
+ responseEntity.contains(expectedFragment));
+ }
+ }
+
+ @Test
+ public void testTraceWithEntity() throws Exception {
+ _testTraceWithEntity(false, false);
+ }
+
+ @Test
+ public void testAsyncTraceWithEntity() throws Exception {
+ _testTraceWithEntity(true, false);
+ }
+
+ @Test
+ public void testTraceWithEntityApacheConnector() throws Exception {
+ _testTraceWithEntity(false, true);
+ }
+
+ @Test
+ public void testAsyncTraceWithEntityApacheConnector() throws Exception {
+ _testTraceWithEntity(true, true);
+ }
+
+ private void _testTraceWithEntity(final boolean isAsync, final boolean useApacheConnection) throws Exception {
+ try {
+ WebTarget target = useApacheConnection ? getApacheClient().target(target().getUri()) : target();
+ target = target.path(ROOT_PATH_ANNOTATED);
+
+ final Entity entity = Entity.entity("trace", MediaType.WILDCARD_TYPE);
+
+ Response response;
+ if (!isAsync) {
+ response = target.request().method(TRACE.NAME, entity);
+ } else {
+ response = target.request().async().method(TRACE.NAME, entity).get();
+ }
+
+ fail("A TRACE request MUST NOT include an entity. (response=" + response + ")");
+ } catch (Exception e) {
+ // OK
+ }
+ }
+
+ private Client getApacheClient() {
+ return ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider()));
+ }
+
+
+ public static String stringify(ContainerRequest request) {
+ StringBuilder buffer = new StringBuilder();
+
+ printRequestLine(buffer, request);
+ printPrefixedHeaders(buffer, request.getHeaders());
+
+ if (request.hasEntity()) {
+ buffer.append(request.readEntity(String.class)).append("\n");
+ }
+
+ return buffer.toString();
+ }
+
+ private static void printRequestLine(StringBuilder buffer, ContainerRequest request) {
+ buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n");
+ }
+
+ private static void printPrefixedHeaders(StringBuilder buffer, Map> headers) {
+ for (Map.Entry> e : headers.entrySet()) {
+ List val = e.getValue();
+ String header = e.getKey();
+
+ if (val.size() == 1) {
+ buffer.append(header).append(": ").append(val.get(0)).append("\n");
+ } else {
+ StringBuilder sb = new StringBuilder();
+ boolean add = false;
+ for (String s : val) {
+ if (add) {
+ sb.append(',');
+ }
+ add = true;
+ sb.append(s);
+ }
+ buffer.append(header).append(": ").append(sb.toString()).append("\n");
+ }
+ }
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java
new file mode 100644
index 0000000000..8248fdd7d8
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+
+import org.apache.hc.client5.http.cookie.CookieStore;
+import org.glassfish.jersey.client.ClientConfig;
+
+import org.junit.Test;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * Test of access to the underlying CookieStore instance used by the connector.
+ *
+ * @author Maksim Mukosey (mmukosey at gmail.com)
+ */
+public class UnderlyingCookieStoreAccessTest {
+
+ @Test
+ public void testCookieStoreInstanceAccess() {
+ final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider()));
+ final CookieStore csOnClient = Apache5ConnectorProvider.getCookieStore(client);
+ // important: the web target instance in this test must be only created AFTER the client has been pre-initialized
+ // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the
+ // connector provider's static getCookieStore method above.
+ final WebTarget target = client.target("http://localhost/");
+ final CookieStore csOnTarget = Apache5ConnectorProvider.getCookieStore(target);
+
+ assertNotNull("CookieStore instance set on JerseyClient should not be null.", csOnClient);
+ assertNotNull("CookieStore instance set on JerseyWebTarget should not be null.", csOnTarget);
+ assertSame("CookieStore instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget"
+ + "(provided the target instance has not been further configured).", csOnClient, csOnTarget);
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java
new file mode 100644
index 0000000000..0c2e320b62
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2022 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.apache5.connector;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.glassfish.jersey.client.ClientConfig;
+
+import org.junit.Test;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * Test of access to the underlying HTTP client instance used by the connector.
+ *
+ * @author Marek Potociar
+ */
+public class UnderlyingHttpClientAccessTest {
+
+ /**
+ * Verifier of JERSEY-2424 fix.
+ */
+ @Test
+ public void testHttpClientInstanceAccess() {
+ final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider()));
+ final HttpClient hcOnClient = Apache5ConnectorProvider.getHttpClient(client);
+ // important: the web target instance in this test must be only created AFTER the client has been pre-initialized
+ // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the
+ // connector provider's static getHttpClient method above.
+ final WebTarget target = client.target("http://localhost/");
+ final HttpClient hcOnTarget = Apache5ConnectorProvider.getHttpClient(target);
+
+ assertNotNull("HTTP client instance set on JerseyClient should not be null.", hcOnClient);
+ assertNotNull("HTTP client instance set on JerseyWebTarget should not be null.", hcOnTarget);
+ assertSame("HTTP client instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget"
+ + "(provided the target instance has not been further configured).",
+ hcOnClient, hcOnTarget
+ );
+ }
+}
diff --git a/connectors/pom.xml b/connectors/pom.xml
index 8266de6355..18fce3cd15 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -1,7 +1,7 @@