Apache httpclient 5 (#4950)
Signed-off-by: Steffen Nießing <zuniquex@protonmail.com>
diff --git a/connectors/apache5-connector/pom.xml b/connectors/apache5-connector/pom.xml
new file mode 100644
index 0000000..669ac11
--- /dev/null
+++ b/connectors/apache5-connector/pom.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+<!--
+
+ 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
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.glassfish.jersey.connectors</groupId>
+ <artifactId>project</artifactId>
+ <version>2.36-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>jersey-apache5-connector</artifactId>
+ <packaging>jar</packaging>
+ <name>jersey-connectors-apache5</name>
+
+ <description>Jersey Client Transport via Apache HttpClient 5.x</description>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.httpcomponents.client5</groupId>
+ <artifactId>httpclient5</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.containers</groupId>
+ <artifactId>jersey-container-grizzly2-http</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+ <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.sun.istack</groupId>
+ <artifactId>istack-commons-maven-plugin</artifactId>
+ <inherited>true</inherited>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <inherited>true</inherited>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <inherited>true</inherited>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ClientProperties.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ClientProperties.java
new file mode 100644
index 0000000..eac5be5
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ClientProperties.java
@@ -0,0 +1,207 @@
+/*
+ * 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.Map;
+
+import org.glassfish.jersey.internal.util.PropertiesClass;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+
+/**
+ * Configuration options specific to the Client API that utilizes {@link Apache5ConnectorProvider}.
+ *
+ * @author jorgeluisw@mac.com
+ * @author Paul Sandoz
+ * @author Pavel Bucek
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Steffen Nießing
+ */
+@PropertiesClass
+public final class Apache5ClientProperties {
+
+ /**
+ * The credential provider that should be used to retrieve
+ * credentials from a user. Credentials needed for proxy authentication
+ * are stored here as well.
+ * <p/>
+ * The value MUST be an instance of {@link org.apache.hc.client5.http.auth.CredentialsProvider}.
+ * <p/>
+ * If the property is absent a default provider will be used.
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ */
+ public static final String CREDENTIALS_PROVIDER = "jersey.config.apache5.client.credentialsProvider";
+
+ /**
+ * A value of {@code false} indicates the client should handle cookies
+ * automatically using HttpClient's default cookie policy. A value
+ * of {@code true} will cause the client to ignore all cookies.
+ * <p/>
+ * The value MUST be an instance of {@link java.lang.Boolean}.
+ * <p/>
+ * The default value is {@code false}.
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ */
+ public static final String DISABLE_COOKIES = "jersey.config.apache5.client.handleCookies";
+
+ /**
+ * A value of {@code true} indicates that a client should send an
+ * authentication request even before the server gives a 401
+ * response.
+ * <p>
+ * This property may only be set prior to constructing Apache connector using {@link Apache5ConnectorProvider}.
+ * <p/>
+ * The value MUST be an instance of {@link java.lang.Boolean}.
+ * <p/>
+ * The default value is {@code false}.
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ */
+ public static final String PREEMPTIVE_BASIC_AUTHENTICATION = "jersey.config.apache5.client.preemptiveBasicAuthentication";
+
+ /**
+ * Connection Manager which will be used to create {@link org.apache.hc.client5.http.classic.HttpClient}.
+ * <p/>
+ * The value MUST be an instance of {@link org.apache.hc.client5.http.io.HttpClientConnectionManager}.
+ * <p/>
+ * If the property is absent a default Connection Manager will be used
+ * ({@link org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager}).
+ * If you want to use this client in multi-threaded environment, be sure you override default value with
+ * {@link org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager} instance.
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ */
+ public static final String CONNECTION_MANAGER = "jersey.config.apache5.client.connectionManager";
+
+ /**
+ * A value of {@code true} indicates that configured connection manager should be shared
+ * among multiple Jersey {@link org.glassfish.jersey.client.ClientRuntime} instances. It means that closing
+ * a particular {@link org.glassfish.jersey.client.ClientRuntime} instance does not shut down the underlying
+ * connection manager automatically. In such case, the connection manager life-cycle
+ * should be fully managed by the application code. To release all allocated resources,
+ * caller code should especially ensure {@link org.apache.hc.client5.http.io.HttpClientConnectionManager#close()} gets
+ * invoked eventually.
+ * <p>
+ * This property may only be set prior to constructing Apache connector using {@link Apache5ConnectorProvider}.
+ * <p/>
+ * The value MUST be an instance of {@link java.lang.Boolean}.
+ * <p/>
+ * The default value is {@code false}.
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ *
+ * @since 2.18
+ */
+ public static final String CONNECTION_MANAGER_SHARED = "jersey.config.apache5.client.connectionManagerShared";
+
+ /**
+ * Request configuration for the {@link org.apache.hc.client5.http.classic.HttpClient}.
+ * Http parameters which will be used to create {@link org.apache.hc.client5.http.classic.HttpClient}.
+ * <p/>
+ * The value MUST be an instance of {@link org.apache.hc.client5.http.config.RequestConfig}.
+ * <p/>
+ * If the property is absent default request configuration will be used.
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ *
+ * @since 2.5
+ */
+ public static final String REQUEST_CONFIG = "jersey.config.apache5.client.requestConfig";
+
+ /**
+ * HttpRequestRetryHandler which will be used to create {@link org.apache.hc.client5.http.classic.HttpClient}.
+ * <p/>
+ * The value MUST be an instance of {@link org.apache.hc.client5.http.HttpRequestRetryStrategy}.
+ * <p/>
+ * If the property is absent a default retry handler will be used
+ * ({@link org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy}).
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ */
+ public static final String RETRY_STRATEGY = "jersey.config.apache5.client.retryStrategy";
+
+ /**
+ * ConnectionReuseStrategy for the {@link org.apache.hc.client5.http.classic.HttpClient}.
+ * <p/>
+ * The value MUST be an instance of {@link org.apache.hc.core5.http.ConnectionReuseStrategy}.
+ * <p/>
+ * If the property is absent the default reuse strategy of the Apache HTTP library will be used
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ */
+ public static final String REUSE_STRATEGY = "jersey.config.apache5.client.reuseStrategy";
+
+ /**
+ * ConnectionKeepAliveStrategy for the {@link org.apache.hc.client5.http.classic.HttpClient}.
+ * <p/>
+ * The value MUST be an instance of {@link org.apache.hc.client5.http.ConnectionKeepAliveStrategy}.
+ * <p/>
+ * If the property is absent the default keepalive strategy of the Apache HTTP library will be used
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ */
+ public static final String KEEPALIVE_STRATEGY = "jersey.config.apache5.client.keepAliveStrategy";
+
+
+ /**
+ * Strategy that closes the Apache Connection. Accepts an instance of {@link Apache5ConnectionClosingStrategy}.
+ *
+ * @see Apache5ConnectionClosingStrategy
+ * @since 2.30
+ */
+ public static final String CONNECTION_CLOSING_STRATEGY = "jersey.config.apache5.client.connectionClosingStrategy";
+
+ /**
+ * A value of {@code false} indicates the client will use default ApacheConnector params. A value
+ * of {@code true} will cause the client to take into account the system properties
+ * {@code https.protocols}, {@code https.cipherSuites}, {@code http.keepAlive},
+ * {@code http.maxConnections}.
+ * <p/>
+ * The value MUST be an instance of {@link java.lang.Boolean}.
+ * <p/>
+ * The default value is {@code false}.
+ * <p/>
+ * The name of the configuration property is <tt>{@value}</tt>.
+ */
+ public static final String USE_SYSTEM_PROPERTIES = "jersey.config.apache5.client.useSystemProperties";
+
+ /**
+ * Get the value of the specified property.
+ *
+ * If the property is not set or the actual property value type is not compatible with the specified type, the method will
+ * return {@code null}.
+ *
+ * @param properties Map of properties to get the property value from.
+ * @param key Name of the property.
+ * @param type Type to retrieve the value as.
+ * @param <T> Type of the property value.
+ * @return Value of the property or {@code null}.
+ *
+ * @since 2.8
+ */
+ public static <T> T getValue(final Map<String, ?> properties, final String key, final Class<T> type) {
+ return PropertiesHelper.getValue(properties, key, type, null);
+ }
+
+ /**
+ * Prevents instantiation.
+ */
+ private Apache5ClientProperties() {
+ throw new AssertionError("No instances allowed.");
+ }
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectionClosingStrategy.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectionClosingStrategy.java
new file mode 100644
index 0000000..ad23f2d
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectionClosingStrategy.java
@@ -0,0 +1,86 @@
+/*
+ * 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.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.glassfish.jersey.client.ClientRequest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+
+/**
+ * Strategy that defines the way the Apache client releases resources. The client enables closing the content stream
+ * and the response. From the Apache documentation:
+ * <pre>
+ * The difference between closing the content stream and closing the response is that
+ * the former will attempt to keep the underlying connection alive by consuming the
+ * entity content while the latter immediately shuts down and discards the connection.
+ * </pre>
+ * In the case of Chunk content stream, the stream is not closed on the server side, and the client can hang on reading
+ * the closing chunk. Using the {@link org.glassfish.jersey.client.ClientProperties#READ_TIMEOUT} property can prevent
+ * this hanging forever and the reading of the closing chunk is terminated when the time is out. The other option, when
+ * the timeout is not set, is to abort the Apache client request. This is the default for Apache Client 4.5.1+ when the
+ * read timeout is not set.
+ * <p/>
+ * Another option is not to close the content stream, which is possible by the Apache client documentation. In this case,
+ * however, the server side may not be notified and would not close its chunk stream.
+ */
+public interface Apache5ConnectionClosingStrategy {
+ /**
+ * Method to close the connection.
+ * @param clientRequest The {@link ClientRequest} to get {@link ClientRequest#getConfiguration() configuration},
+ * and {@link ClientRequest#resolveProperty(String, Class) resolve properties}.
+ * @param request Apache {@code HttpUriRequest} that can be {@code abort}ed.
+ * @param response Apache {@code CloseableHttpResponse} that can be {@code close}d.
+ * @param stream The entity stream that can be {@link InputStream#close() closed}.
+ * @throws IOException In case of some of the closing methods throws {@link IOException}
+ */
+ void close(ClientRequest clientRequest, HttpUriRequest request, CloseableHttpResponse response, InputStream stream)
+ throws IOException;
+
+ /**
+ * Strategy that aborts Apache HttpRequests for the case of Chunked Stream, closes the stream, and response next.
+ */
+ class Apache5GracefulClosingStrategy implements Apache5ConnectionClosingStrategy {
+ private static final String UNIX_PROTOCOL = "unix";
+
+ static final Apache5GracefulClosingStrategy INSTANCE = new Apache5GracefulClosingStrategy();
+
+ @Override
+ public void close(ClientRequest clientRequest, HttpUriRequest request, CloseableHttpResponse response, InputStream stream)
+ throws IOException {
+ boolean isUnixProtocol = false;
+ try {
+ isUnixProtocol = UNIX_PROTOCOL.equals(request.getUri().getScheme());
+ } catch (URISyntaxException ex) {
+ // Ignore
+ }
+ if (response.getEntity() != null && response.getEntity().isChunked() && !isUnixProtocol) {
+ request.abort();
+ }
+ try {
+ stream.close();
+ } catch (IOException ex) {
+ // Ignore
+ } finally {
+ response.close();
+ }
+ }
+ }
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java
new file mode 100644
index 0000000..13b8279
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java
@@ -0,0 +1,814 @@
+/*
+ * 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.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.auth.AuthCache;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.CredentialsStore;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.cookie.BasicCookieStore;
+import org.apache.hc.client5.http.cookie.CookieStore;
+import org.apache.hc.client5.http.cookie.StandardCookieSpec;
+import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
+import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
+import org.apache.hc.client5.http.impl.auth.BasicScheme;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
+import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory;
+import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.core5.http.ConnectionReuseStrategy;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.impl.DefaultContentLengthStrategy;
+import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
+import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.apache.hc.core5.util.TextUtils;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.hc.core5.util.VersionInfo;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.message.internal.HeaderUtils;
+import org.glassfish.jersey.message.internal.OutboundMessageContext;
+import org.glassfish.jersey.message.internal.ReaderWriter;
+import org.glassfish.jersey.message.internal.Statuses;
+
+/**
+ * A {@link Connector} that utilizes the Apache HTTP Client to send and receive
+ * HTTP request and responses.
+ * <p/>
+ * The following properties are only supported at construction of this class:
+ * <ul>
+ * <li>{@link Apache5ClientProperties#CONNECTION_MANAGER}</li>
+ * <li>{@link Apache5ClientProperties#REQUEST_CONFIG}</li>
+ * <li>{@link Apache5ClientProperties#CREDENTIALS_PROVIDER}</li>
+ * <li>{@link Apache5ClientProperties#DISABLE_COOKIES}</li>
+ * <li>{@link Apache5ClientProperties#KEEPALIVE_STRATEGY}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING} - default value is {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED}</li>
+ * <li>{@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}</li>
+ * <li>{@link Apache5ClientProperties#RETRY_STRATEGY}</li>
+ * <li>{@link Apache5ClientProperties#REUSE_STRATEGY}</li>
+ * </ul>
+ * <p>
+ * This connector uses {@link RequestEntityProcessing#CHUNKED chunked encoding} as a default setting. This can
+ * be overridden by the {@link ClientProperties#REQUEST_ENTITY_PROCESSING}. By default the
+ * {@link ClientProperties#CHUNKED_ENCODING_SIZE} property is only supported by using default connection manager. If custom
+ * connection manager needs to be used then chunked encoding size can be set by providing a custom
+ * {@link org.apache.hc.core5.http.io.HttpClientConnection} (via custom {@link org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory})
+ * and overriding {@code createOutputStream} method.
+ * </p>
+ * <p>
+ * Using of authorization is dependent on the chunk encoding setting. If the entity
+ * buffering is enabled, the entity is buffered and authorization can be performed
+ * automatically in response to a 401 by sending the request again. When entity buffering
+ * is disabled (chunked encoding is used) then the property
+ * {@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION} must
+ * be set to {@code true}.
+ * </p>
+ * <p>
+ * Registration of {@link Apache5HttpClientBuilderConfigurator} instance on the
+ * {@link javax.ws.rs.client.Client#register(Object) Client} is supported. A configuration provided by
+ * {@link Apache5HttpClientBuilderConfigurator} will override the {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}
+ * configuration set by using the properties.
+ * </p>
+ * <p>
+ * If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an
+ * entity is not read from the response then
+ * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called
+ * after processing the response to release connection-based resources.
+ * </p>
+ * <p>
+ * Client operations are thread safe, the HTTP connection may
+ * be shared between different threads.
+ * </p>
+ * <p>
+ * If a response entity is obtained that is an instance of {@link Closeable}
+ * then the instance MUST be closed after processing the entity to release
+ * connection-based resources.
+ * </p>
+ * <p>
+ * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE.
+ * </p>
+ *
+ * @author jorgeluisw@mac.com
+ * @author Paul Sandoz
+ * @author Pavel Bucek
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Steffen Nießing
+ * @see Apache5ClientProperties#CONNECTION_MANAGER
+ */
+class Apache5Connector implements Connector {
+
+ private static final Logger LOGGER = Logger.getLogger(Apache5Connector.class.getName());
+ private static final VersionInfo vi;
+ private static final String release;
+
+ static {
+ vi = VersionInfo.loadVersionInfo("org.apache.hc.client5", HttpClientBuilder.class.getClassLoader());
+ release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
+ }
+
+ private final CloseableHttpClient client;
+ private final CookieStore cookieStore;
+ private final boolean preemptiveBasicAuth;
+ private final RequestConfig requestConfig;
+
+ /**
+ * Create the new Apache HTTP Client connector.
+ *
+ * @param client JAX-RS client instance for which the connector is being created.
+ * @param config client configuration.
+ */
+ Apache5Connector(final Client client, final Configuration config) {
+ final Object connectionManager = config.getProperties().get(Apache5ClientProperties.CONNECTION_MANAGER);
+ if (connectionManager != null) {
+ if (!(connectionManager instanceof HttpClientConnectionManager)) {
+ LOGGER.log(
+ Level.WARNING,
+ LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+ Apache5ClientProperties.CONNECTION_MANAGER,
+ connectionManager.getClass().getName(),
+ HttpClientConnectionManager.class.getName())
+ );
+ }
+ }
+
+ Object keepAliveStrategy = config.getProperties().get(Apache5ClientProperties.KEEPALIVE_STRATEGY);
+ if (keepAliveStrategy != null) {
+ if (!(keepAliveStrategy instanceof ConnectionKeepAliveStrategy)) {
+ LOGGER.log(
+ Level.WARNING,
+ LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+ Apache5ClientProperties.KEEPALIVE_STRATEGY,
+ keepAliveStrategy.getClass().getName(),
+ ConnectionKeepAliveStrategy.class.getName())
+ );
+ keepAliveStrategy = null;
+ }
+ }
+
+ Object reuseStrategy = config.getProperties().get(Apache5ClientProperties.REUSE_STRATEGY);
+ if (reuseStrategy != null) {
+ if (!(reuseStrategy instanceof ConnectionReuseStrategy)) {
+ LOGGER.log(
+ Level.WARNING,
+ LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+ Apache5ClientProperties.REUSE_STRATEGY,
+ reuseStrategy.getClass().getName(),
+ ConnectionReuseStrategy.class.getName())
+ );
+ reuseStrategy = null;
+ }
+ }
+
+ Object reqConfig = config.getProperties().get(Apache5ClientProperties.REQUEST_CONFIG);
+ if (reqConfig != null) {
+ if (!(reqConfig instanceof RequestConfig)) {
+ LOGGER.log(
+ Level.WARNING,
+ LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+ Apache5ClientProperties.REQUEST_CONFIG,
+ reqConfig.getClass().getName(),
+ RequestConfig.class.getName())
+ );
+ reqConfig = null;
+ }
+ }
+
+ final SSLContext sslContext = client.getSslContext();
+ final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
+
+ clientBuilder.setConnectionManager(getConnectionManager(client, config, sslContext));
+ clientBuilder.setConnectionManagerShared(
+ PropertiesHelper.getValue(
+ config.getProperties(),
+ Apache5ClientProperties.CONNECTION_MANAGER_SHARED,
+ false,
+ null
+ )
+ );
+ if (keepAliveStrategy != null) {
+ clientBuilder.setKeepAliveStrategy((ConnectionKeepAliveStrategy) keepAliveStrategy);
+ }
+ if (reuseStrategy != null) {
+ clientBuilder.setConnectionReuseStrategy((ConnectionReuseStrategy) reuseStrategy);
+ }
+
+ final RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
+
+ final Object credentialsProvider = config.getProperty(Apache5ClientProperties.CREDENTIALS_PROVIDER);
+ if (credentialsProvider != null && (credentialsProvider instanceof CredentialsProvider)) {
+ clientBuilder.setDefaultCredentialsProvider((CredentialsProvider) credentialsProvider);
+ }
+
+ final Object retryHandler = config.getProperties().get(Apache5ClientProperties.RETRY_STRATEGY);
+ if (retryHandler != null && (retryHandler instanceof HttpRequestRetryStrategy)) {
+ clientBuilder.setRetryStrategy((HttpRequestRetryStrategy) retryHandler);
+ }
+
+ final Object proxyUri;
+ proxyUri = config.getProperty(ClientProperties.PROXY_URI);
+ if (proxyUri != null) {
+ final URI u = getProxyUri(proxyUri);
+ final HttpHost proxy = new HttpHost(u.getScheme(), u.getHost(), u.getPort());
+ final String userName;
+ userName = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_USERNAME, String.class);
+ if (userName != null) {
+ final String password;
+ password = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class);
+
+ if (password != null) {
+ final CredentialsStore credsProvider = new BasicCredentialsProvider();
+ credsProvider.setCredentials(
+ new AuthScope(u.getHost(), u.getPort()),
+ new UsernamePasswordCredentials(userName, password.toCharArray())
+ );
+ clientBuilder.setDefaultCredentialsProvider(credsProvider);
+ }
+ }
+ clientBuilder.setProxy(proxy);
+ }
+
+ final Boolean preemptiveBasicAuthProperty = (Boolean) config.getProperties()
+ .get(Apache5ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION);
+ this.preemptiveBasicAuth = (preemptiveBasicAuthProperty != null) ? preemptiveBasicAuthProperty : false;
+
+ final boolean ignoreCookies = PropertiesHelper.isProperty(
+ config.getProperties(),
+ Apache5ClientProperties.DISABLE_COOKIES
+ );
+
+ if (reqConfig != null) {
+ final RequestConfig.Builder reqConfigBuilder = RequestConfig.copy((RequestConfig) reqConfig);
+ if (ignoreCookies) {
+ reqConfigBuilder.setCookieSpec(StandardCookieSpec.IGNORE);
+ }
+ requestConfig = reqConfigBuilder.build();
+ } else {
+ if (ignoreCookies) {
+ requestConfigBuilder.setCookieSpec(StandardCookieSpec.IGNORE);
+ }
+ requestConfig = requestConfigBuilder.build();
+ }
+
+ if (requestConfig.getCookieSpec() == null || !requestConfig.getCookieSpec().equals(StandardCookieSpec.IGNORE)) {
+ this.cookieStore = new BasicCookieStore();
+ clientBuilder.setDefaultCookieStore(cookieStore);
+ } else {
+ this.cookieStore = null;
+ }
+ clientBuilder.setDefaultRequestConfig(requestConfig);
+
+ LinkedList<Object> contracts = config.getInstances().stream()
+ .filter(Apache5HttpClientBuilderConfigurator.class::isInstance)
+ .collect(Collectors.toCollection(LinkedList::new));
+
+ HttpClientBuilder configuredBuilder = clientBuilder;
+ for (Object configurator : contracts) {
+ configuredBuilder = ((Apache5HttpClientBuilderConfigurator) configurator).configure(configuredBuilder);
+ }
+
+ this.client = configuredBuilder.build();
+ }
+
+ private HttpClientConnectionManager getConnectionManager(final Client client,
+ final Configuration config,
+ final SSLContext sslContext) {
+ final Object cmObject = config.getProperties().get(Apache5ClientProperties.CONNECTION_MANAGER);
+
+ // Connection manager from configuration.
+ if (cmObject != null) {
+ if (cmObject instanceof HttpClientConnectionManager) {
+ return (HttpClientConnectionManager) cmObject;
+ } else {
+ LOGGER.log(
+ Level.WARNING,
+ LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+ Apache5ClientProperties.CONNECTION_MANAGER,
+ cmObject.getClass().getName(),
+ HttpClientConnectionManager.class.getName())
+ );
+ }
+ }
+
+ final boolean useSystemProperties =
+ PropertiesHelper.isProperty(config.getProperties(), Apache5ClientProperties.USE_SYSTEM_PROPERTIES);
+
+ // Create custom connection manager.
+ return createConnectionManager(
+ client,
+ config,
+ sslContext,
+ useSystemProperties);
+ }
+
+ private HttpClientConnectionManager createConnectionManager(
+ final Client client,
+ final Configuration config,
+ final SSLContext sslContext,
+ final boolean useSystemProperties) {
+
+ final String[] supportedProtocols = useSystemProperties ? split(
+ System.getProperty("https.protocols")) : null;
+ final String[] supportedCipherSuites = useSystemProperties ? split(
+ System.getProperty("https.cipherSuites")) : null;
+
+ HostnameVerifier hostnameVerifier = client.getHostnameVerifier();
+
+ final LayeredConnectionSocketFactory sslSocketFactory;
+ if (sslContext != null) {
+ sslSocketFactory = new SSLConnectionSocketFactory(
+ sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+ } else {
+ if (useSystemProperties) {
+ sslSocketFactory = new SSLConnectionSocketFactory(
+ (SSLSocketFactory) SSLSocketFactory.getDefault(),
+ supportedProtocols, supportedCipherSuites, hostnameVerifier);
+ } else {
+ sslSocketFactory = new SSLConnectionSocketFactory(
+ SSLContexts.createDefault(),
+ hostnameVerifier);
+ }
+ }
+
+ final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", sslSocketFactory)
+ .build();
+
+ final Integer chunkSize = ClientProperties.getValue(config.getProperties(),
+ ClientProperties.CHUNKED_ENCODING_SIZE, ClientProperties.DEFAULT_CHUNK_SIZE, Integer.class);
+
+ final PoolingHttpClientConnectionManager connectionManager =
+ new PoolingHttpClientConnectionManager(registry, new ConnectionFactory(chunkSize));
+
+ if (useSystemProperties) {
+ String s = System.getProperty("http.keepAlive", "true");
+ if ("true".equalsIgnoreCase(s)) {
+ s = System.getProperty("http.maxConnections", "5");
+ final int max = Integer.parseInt(s);
+ connectionManager.setDefaultMaxPerRoute(max);
+ connectionManager.setMaxTotal(2 * max);
+ }
+ }
+
+ return connectionManager;
+ }
+
+ private static String[] split(final String s) {
+ if (TextUtils.isBlank(s)) {
+ return null;
+ }
+ return s.split(" *, *");
+ }
+
+ /**
+ * Get the {@link HttpClient}.
+ *
+ * @return the {@link HttpClient}.
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public HttpClient getHttpClient() {
+ return client;
+ }
+
+ /**
+ * Get the {@link CookieStore}.
+ *
+ * @return the {@link CookieStore} instance or {@code null} when {@value Apache5ClientProperties#DISABLE_COOKIES} set to
+ * {@code true}.
+ */
+ public CookieStore getCookieStore() {
+ return cookieStore;
+ }
+
+ private static URI getProxyUri(final Object proxy) {
+ if (proxy instanceof URI) {
+ return (URI) proxy;
+ } else if (proxy instanceof String) {
+ return URI.create((String) proxy);
+ } else {
+ throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI));
+ }
+ }
+
+ @Override
+ public ClientResponse apply(final ClientRequest clientRequest) throws ProcessingException {
+ final HttpUriRequest request = getUriHttpRequest(clientRequest);
+ final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(clientRequest, request);
+
+ try {
+ final CloseableHttpResponse response;
+ final HttpClientContext context = HttpClientContext.create();
+ if (preemptiveBasicAuth) {
+ final AuthCache authCache = new BasicAuthCache();
+ final BasicScheme basicScheme = new BasicScheme();
+ authCache.put(getHost(request), basicScheme);
+ context.setAuthCache(authCache);
+ }
+
+ // If a request-specific CredentialsProvider exists, use it instead of the default one
+ CredentialsProvider credentialsProvider =
+ clientRequest.resolveProperty(Apache5ClientProperties.CREDENTIALS_PROVIDER, CredentialsProvider.class);
+ if (credentialsProvider != null) {
+ context.setCredentialsProvider(credentialsProvider);
+ }
+
+ response = client.execute(getHost(request), request, context);
+ HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, clientRequest.getHeaders(),
+ this.getClass().getName(), clientRequest.getConfiguration());
+
+ final Response.StatusType status = response.getReasonPhrase() == null
+ ? Statuses.from(response.getCode())
+ : Statuses.from(response.getCode(), response.getReasonPhrase());
+
+ final ClientResponse responseContext = new ClientResponse(status, clientRequest);
+ final List<URI> redirectLocations = context.getRedirectLocations().getAll();
+ if (redirectLocations != null && !redirectLocations.isEmpty()) {
+ responseContext.setResolvedRequestUri(redirectLocations.get(redirectLocations.size() - 1));
+ }
+
+ final Header[] respHeaders = response.getHeaders();
+ final MultivaluedMap<String, String> headers = responseContext.getHeaders();
+ for (final Header header : respHeaders) {
+ final String headerName = header.getName();
+ List<String> list = headers.get(headerName);
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ list.add(header.getValue());
+ headers.put(headerName, list);
+ }
+
+ final HttpEntity entity = response.getEntity();
+
+ if (entity != null) {
+ if (headers.get(HttpHeaders.CONTENT_LENGTH) == null) {
+ headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(entity.getContentLength()));
+ }
+
+ final String contentEncoding = entity.getContentEncoding();
+ if (headers.get(HttpHeaders.CONTENT_ENCODING) == null && contentEncoding != null && !contentEncoding.isEmpty()) {
+ headers.add(HttpHeaders.CONTENT_ENCODING, contentEncoding);
+ }
+ }
+
+ try {
+ final ConnectionClosingMechanism closingMechanism = new ConnectionClosingMechanism(clientRequest, request);
+ responseContext.setEntityStream(getInputStream(response, closingMechanism));
+ } catch (final IOException e) {
+ LOGGER.log(Level.SEVERE, null, e);
+ }
+
+ return responseContext;
+ } catch (final Exception e) {
+ throw new ProcessingException(e);
+ }
+ }
+
+ @Override
+ public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) {
+ try {
+ ClientResponse response = apply(request);
+ callback.response(response);
+ return CompletableFuture.completedFuture(response);
+ } catch (Throwable t) {
+ callback.failure(t);
+ CompletableFuture<Object> future = new CompletableFuture<>();
+ future.completeExceptionally(t);
+ return future;
+ }
+ }
+
+ @Override
+ public String getName() {
+ return "Apache HttpClient " + release;
+ }
+
+ @Override
+ public void close() {
+ try {
+ client.close();
+ } catch (final IOException e) {
+ throw new ProcessingException(LocalizationMessages.FAILED_TO_STOP_CLIENT(), e);
+ }
+ }
+
+ private HttpHost getHost(final HttpUriRequest request) throws URISyntaxException {
+ return new HttpHost(request.getUri().getScheme(), request.getUri().getHost(), request.getUri().getPort());
+ }
+
+ private HttpUriRequest getUriHttpRequest(final ClientRequest clientRequest) {
+ final RequestConfig.Builder requestConfigBuilder = RequestConfig.copy(requestConfig);
+
+ final int connectTimeout = clientRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, -1);
+ final int socketTimeout = clientRequest.resolveProperty(ClientProperties.READ_TIMEOUT, -1);
+
+ if (connectTimeout >= 0) {
+ requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectTimeout));
+ }
+ if (socketTimeout >= 0) {
+ requestConfigBuilder.setResponseTimeout(Timeout.ofMilliseconds(socketTimeout));
+ }
+
+ final Boolean redirectsEnabled =
+ clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, requestConfig.isRedirectsEnabled());
+ requestConfigBuilder.setRedirectsEnabled(redirectsEnabled);
+
+ final Boolean bufferingEnabled = clientRequest.resolveProperty(ClientProperties.REQUEST_ENTITY_PROCESSING,
+ RequestEntityProcessing.class) == RequestEntityProcessing.BUFFERED;
+ final HttpEntity entity = getHttpEntity(clientRequest, bufferingEnabled);
+
+ HttpUriRequestBase httpUriRequestBase = new HttpUriRequestBase(clientRequest.getMethod(), clientRequest.getUri());
+ httpUriRequestBase.setConfig(requestConfigBuilder.build());
+ httpUriRequestBase.setEntity(entity);
+
+ return httpUriRequestBase;
+ }
+
+ private HttpEntity getHttpEntity(final ClientRequest clientRequest, final boolean bufferingEnabled) {
+ final Object entity = clientRequest.getEntity();
+
+ if (entity == null) {
+ return null;
+ }
+
+ if (HttpEntity.class.isInstance(entity)) {
+ return wrapHttpEntity(clientRequest, (HttpEntity) entity);
+ }
+
+ String contentType = clientRequest.getHeaderString(HttpHeaders.CONTENT_TYPE);
+ String contentEncoding = clientRequest.getHeaderString(HttpHeaders.CONTENT_ENCODING);
+
+ final AbstractHttpEntity httpEntity = new AbstractHttpEntity(contentType, contentEncoding) {
+ @Override
+ public void close() throws IOException {
+
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ @Override
+ public long getContentLength() {
+ return -1;
+ }
+
+ @Override
+ public InputStream getContent() throws IOException, IllegalStateException {
+ if (bufferingEnabled) {
+ final ByteArrayOutputStream buffer = new ByteArrayOutputStream(512);
+ writeTo(buffer);
+ return new ByteArrayInputStream(buffer.toByteArray());
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void writeTo(final OutputStream outputStream) throws IOException {
+ clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
+ @Override
+ public OutputStream getOutputStream(final int contentLength) throws IOException {
+ return outputStream;
+ }
+ });
+ clientRequest.writeEntity();
+ }
+
+ @Override
+ public boolean isStreaming() {
+ return false;
+ }
+ };
+
+ return bufferEntity(httpEntity, bufferingEnabled);
+ }
+
+ private HttpEntity wrapHttpEntity(final ClientRequest clientRequest, final HttpEntity originalEntity) {
+ final boolean bufferingEnabled = BufferedHttpEntity.class.isInstance(originalEntity);
+
+ try {
+ clientRequest.setEntity(originalEntity.getContent());
+ } catch (IOException e) {
+ throw new ProcessingException(LocalizationMessages.ERROR_READING_HTTPENTITY_STREAM(e.getMessage()), e);
+ }
+
+ final AbstractHttpEntity httpEntity = new AbstractHttpEntity(
+ originalEntity.getContentType(),
+ originalEntity.getContentEncoding(),
+ originalEntity.isChunked()
+ ) {
+ @Override
+ public void close() throws IOException {
+
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return originalEntity.isRepeatable();
+ }
+
+ @Override
+ public long getContentLength() {
+ return originalEntity.getContentLength();
+ }
+
+ @Override
+ public InputStream getContent() throws IOException, IllegalStateException {
+ return originalEntity.getContent();
+ }
+
+ @Override
+ public void writeTo(final OutputStream outputStream) throws IOException {
+ clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
+ @Override
+ public OutputStream getOutputStream(final int contentLength) throws IOException {
+ return outputStream;
+ }
+ });
+ clientRequest.writeEntity();
+ }
+
+ @Override
+ public boolean isStreaming() {
+ return originalEntity.isStreaming();
+ }
+ };
+
+ return bufferEntity(httpEntity, bufferingEnabled);
+ }
+
+ private static HttpEntity bufferEntity(HttpEntity httpEntity, boolean bufferingEnabled) {
+ if (bufferingEnabled) {
+ try {
+ return new BufferedHttpEntity(httpEntity);
+ } catch (final IOException e) {
+ throw new ProcessingException(LocalizationMessages.ERROR_BUFFERING_ENTITY(), e);
+ }
+ } else {
+ return httpEntity;
+ }
+ }
+
+ private static Map<String, String> writeOutBoundHeaders(final ClientRequest clientRequest,
+ final HttpUriRequest request) {
+ final Map<String, String> stringHeaders =
+ HeaderUtils.asStringHeadersSingleValue(clientRequest.getHeaders(), clientRequest.getConfiguration());
+
+ for (final Map.Entry<String, String> e : stringHeaders.entrySet()) {
+ request.addHeader(e.getKey(), e.getValue());
+ }
+ return stringHeaders;
+ }
+
+ private static InputStream getInputStream(final CloseableHttpResponse response,
+ final ConnectionClosingMechanism closingMechanism) throws IOException {
+ final InputStream inputStream;
+
+ if (response.getEntity() == null) {
+ inputStream = new ByteArrayInputStream(new byte[0]);
+ } else {
+ final InputStream i = response.getEntity().getContent();
+ if (i.markSupported()) {
+ inputStream = i;
+ } else {
+ inputStream = new BufferedInputStream(i, ReaderWriter.BUFFER_SIZE);
+ }
+ }
+
+ return closingMechanism.getEntityStream(inputStream, response);
+ }
+
+ /**
+ * The way the Apache CloseableHttpResponse is to be closed.
+ * See https://github.com/eclipse-ee4j/jersey/issues/4321
+ * {@link Apache5ClientProperties#CONNECTION_CLOSING_STRATEGY}
+ */
+ private final class ConnectionClosingMechanism {
+ private Apache5ConnectionClosingStrategy connectionClosingStrategy = null;
+ private final ClientRequest clientRequest;
+ private final HttpUriRequest apacheRequest;
+
+ private ConnectionClosingMechanism(ClientRequest clientRequest, HttpUriRequest apacheRequest) {
+ this.clientRequest = clientRequest;
+ this.apacheRequest = apacheRequest;
+ Object closingStrategyProperty = clientRequest
+ .resolveProperty(Apache5ClientProperties.CONNECTION_CLOSING_STRATEGY, Object.class);
+ if (closingStrategyProperty != null) {
+ if (Apache5ConnectionClosingStrategy.class.isInstance(closingStrategyProperty)) {
+ connectionClosingStrategy = (Apache5ConnectionClosingStrategy) closingStrategyProperty;
+ } else {
+ LOGGER.log(
+ Level.WARNING,
+ LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+ Apache5ClientProperties.CONNECTION_CLOSING_STRATEGY,
+ closingStrategyProperty,
+ Apache5ConnectionClosingStrategy.class.getName())
+ );
+ }
+ }
+
+ if (connectionClosingStrategy == null) {
+ connectionClosingStrategy = Apache5ConnectionClosingStrategy.Apache5GracefulClosingStrategy.INSTANCE;
+ }
+ }
+
+ private InputStream getEntityStream(final InputStream inputStream,
+ final CloseableHttpResponse response) {
+ InputStream filterStream = new FilterInputStream(inputStream) {
+ @Override
+ public void close() throws IOException {
+ connectionClosingStrategy.close(clientRequest, apacheRequest, response, in);
+ }
+ };
+ return filterStream;
+ }
+ }
+
+ private static class ConnectionFactory extends ManagedHttpClientConnectionFactory {
+ private ConnectionFactory(final int chunkSize) {
+ super(
+ Http1Config.custom().setChunkSizeHint(chunkSize).build(),
+ null,
+ null,
+ null,
+ DefaultContentLengthStrategy.INSTANCE,
+ DefaultContentLengthStrategy.INSTANCE
+ );
+ }
+ }
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectorProvider.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectorProvider.java
new file mode 100644
index 0000000..9a32a2c
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectorProvider.java
@@ -0,0 +1,157 @@
+/*
+ * 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.core.Configurable;
+import javax.ws.rs.core.Configuration;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.cookie.CookieStore;
+import org.glassfish.jersey.client.Initializable;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+
+/**
+ * Connector provider for Jersey {@link Connector connectors} that utilize
+ * Apache HTTP Client to send and receive HTTP request and responses.
+ * <p>
+ * The following connector configuration properties are supported:
+ * <ul>
+ * <li>{@link Apache5ClientProperties#CONNECTION_MANAGER}</li>
+ * <li>{@link Apache5ClientProperties#REQUEST_CONFIG}</li>
+ * <li>{@link Apache5ClientProperties#CREDENTIALS_PROVIDER}</li>
+ * <li>{@link Apache5ClientProperties#DISABLE_COOKIES}</li>
+ * <li>{@link Apache5ClientProperties#KEEPALIVE_STRATEGY}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING}
+ * - default value is {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED}</li>
+ * <li>{@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}</li>
+ * <li>{@link Apache5ClientProperties#RETRY_STRATEGY}</li>
+ * <li>{@link Apache5ClientProperties#REUSE_STRATEGY}</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Connector instances created via this connector provider use
+ * {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED chunked encoding} as a default setting.
+ * This can be overridden by the {@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING}.
+ * By default the {@link org.glassfish.jersey.client.ClientProperties#CHUNKED_ENCODING_SIZE} property is only supported
+ * when using the default {@link org.apache.hc.core5.http.io.HttpClientConnection} instance. If custom
+ * connection manager is used, then chunked encoding size can be set by providing a custom
+ * {@link org.apache.hc.core5.http.io.HttpClientConnection} (via custom {@link org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory})
+ * and overriding it's {@code createOutputStream} method.
+ * </p>
+ * <p>
+ * Use of authorization by the AHC-based connectors is dependent on the chunk encoding setting.
+ * If the entity buffering is enabled, the entity is buffered and authorization can be performed
+ * automatically in response to a 401 by sending the request again. When entity buffering
+ * is disabled (chunked encoding is used) then the property
+ * {@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION} must
+ * be set to {@code true}.
+ * </p>
+ * <p>
+ * If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an entity is not read from the response then
+ * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called after processing the response to release
+ * connection-based resources.
+ * </p>
+ * <p>
+ * Registration of {@link Apache5HttpClientBuilderConfigurator} instance on the
+ * {@link javax.ws.rs.client.Client#register(Object) Client} is supported. A configuration provided by
+ * {@link Apache5HttpClientBuilderConfigurator} will override the {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}
+ * configuration set by using the properties.
+ * </p>
+ * <p>
+ * If a response entity is obtained that is an instance of {@link java.io.Closeable}
+ * then the instance MUST be closed after processing the entity to release
+ * connection-based resources.
+ * <p/>
+ * <p>
+ * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE.
+ * <p/>
+ *
+ * @author Pavel Bucek
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author jorgeluisw at mac.com
+ * @author Marek Potociar
+ * @author Paul Sandoz
+ * @author Maksim Mukosey (mmukosey at gmail.com)
+ * @since 2.5
+ */
+public class Apache5ConnectorProvider implements ConnectorProvider {
+
+ @Override
+ public Connector getConnector(final Client client, final Configuration runtimeConfig) {
+ return new Apache5Connector(client, runtimeConfig);
+ }
+
+ /**
+ * Retrieve the underlying Apache {@link org.apache.hc.client5.http.classic.HttpClient} instance from
+ * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget}
+ * configured to use {@code ApacheConnectorProvider}.
+ *
+ * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use
+ * {@code ApacheConnectorProvider}.
+ * @return underlying Apache {@code HttpClient} instance.
+ *
+ * @throws java.lang.IllegalArgumentException in case the {@code component} is neither {@code JerseyClient}
+ * nor {@code JerseyWebTarget} instance or in case the component
+ * is not configured to use a {@code ApacheConnectorProvider}.
+ * @since 2.8
+ */
+ public static HttpClient getHttpClient(final Configurable<?> component) {
+ return getConnector(component).getHttpClient();
+ }
+
+ /**
+ * Retrieve the underlying Apache {@link CookieStore} instance from
+ * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget}
+ * configured to use {@code ApacheConnectorProvider}.
+ *
+ * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use
+ * {@code ApacheConnectorProvider}.
+ * @return underlying Apache {@code CookieStore} instance.
+ * @throws java.lang.IllegalArgumentException in case the {@code component} is neither {@code JerseyClient}
+ * nor {@code JerseyWebTarget} instance or in case the component
+ * is not configured to use a {@code ApacheConnectorProvider}.
+ * @since 2.16
+ */
+ public static CookieStore getCookieStore(final Configurable<?> component) {
+ return getConnector(component).getCookieStore();
+ }
+
+ private static Apache5Connector getConnector(final Configurable<?> component) {
+ if (!(component instanceof Initializable)) {
+ throw new IllegalArgumentException(
+ LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName()));
+ }
+
+ final Initializable<?> initializable = (Initializable<?>) component;
+ Connector connector = initializable.getConfiguration().getConnector();
+ if (connector == null) {
+ initializable.preInitialize();
+ connector = initializable.getConfiguration().getConnector();
+ }
+
+ if (connector instanceof Apache5Connector) {
+ return (Apache5Connector) connector;
+ } else {
+ throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED());
+ }
+ }
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5HttpClientBuilderConfigurator.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5HttpClientBuilderConfigurator.java
new file mode 100644
index 0000000..d179f5c
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5HttpClientBuilderConfigurator.java
@@ -0,0 +1,36 @@
+/*
+ * 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.client5.http.impl.classic.HttpClientBuilder;
+import org.glassfish.jersey.spi.Contract;
+
+/**
+ * A callback interface used to configure {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}. It is called immediately before
+ * the {@link Apache5ConnectorProvider} creates {@link org.apache.hc.client5.http.classic.HttpClient}, after the
+ * {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder} is configured using the properties.
+ */
+@Contract
+public interface Apache5HttpClientBuilderConfigurator {
+ /**
+ * A callback method to configure the {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}
+ * @param httpClientBuilder {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder} object to be further configured
+ * @return the configured {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}. If {@code null} is returned the
+ * {@code httpClientBuilder} is used by {@link Apache5ConnectorProvider} instead.
+ */
+ HttpClientBuilder configure(HttpClientBuilder httpClientBuilder);
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/package-info.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/package-info.java
new file mode 100644
index 0000000..0b05d4b
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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
+ */
+
+/**
+ * Jersey client {@link org.glassfish.jersey.client.spi.Connector connector} based on the
+ * Apache Http Client.
+ */
+package org.glassfish.jersey.apache5.connector;
diff --git a/connectors/apache5-connector/src/main/resources/org/glassfish/jersey/apache5/connector/localization.properties b/connectors/apache5-connector/src/main/resources/org/glassfish/jersey/apache5/connector/localization.properties
new file mode 100644
index 0000000..16dbc30
--- /dev/null
+++ b/connectors/apache5-connector/src/main/resources/org/glassfish/jersey/apache5/connector/localization.properties
@@ -0,0 +1,25 @@
+#
+# 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
+#
+
+error.buffering.entity=Error buffering the entity.
+error.reading.httpentity.stream=Error reading InputStream from HttpEntity: "{0}"
+failed.to.stop.client=Failed to stop the client.
+# {0} - property name, e.g. jersey.config.client.httpclient.connectionManager; {1}, {2} - full class name
+ignoring.value.of.property=Ignoring value of property "{0}" ("{1}") - not instance of "{2}".
+# {0} - property name - jersey.config.client.httpclient.proxyUri
+wrong.proxy.uri.type=The proxy URI ("{0}") property MUST be an instance of String or URI.
+invalid.configurable.component.type=The supplied component "{0}" is not assignable from JerseyClient or JerseyWebTarget.
+expected.connector.provider.not.used=The supplied component is not configured to use a ApacheConnectorProvider.
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AsyncTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AsyncTest.java
new file mode 100644
index 0000000..50e8f65
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AsyncTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+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.Entity;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.container.TimeoutHandler;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+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.hamcrest.Matchers;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Asynchronous connector test.
+ *
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Marek Potociar
+ */
+public class AsyncTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName());
+ private static final String PATH = "async";
+
+ /**
+ * Asynchronous test resource.
+ */
+ @Path(PATH)
+ public static class AsyncResource {
+ /**
+ * Typical long-running operation duration.
+ */
+ public static final long OPERATION_DURATION = 1000;
+
+ /**
+ * Long-running asynchronous post.
+ *
+ * @param asyncResponse async response.
+ * @param id post request id (received as request payload).
+ */
+ @POST
+ public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) {
+ LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName());
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep()
+ try {
+ Thread.sleep(OPERATION_DURATION);
+ return "DONE-" + id;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "INTERRUPTED-" + id;
+ } finally {
+ LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName());
+ }
+ }
+ }, "async-post-runner-" + id).start();
+ }
+
+ /**
+ * Long-running async get request that times out.
+ *
+ * @param asyncResponse async response.
+ */
+ @GET
+ @Path("timeout")
+ public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+ LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName());
+ asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+ @Override
+ public void handleTimeout(AsyncResponse asyncResponse) {
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity("Operation time out.").build());
+ }
+ });
+ asyncResponse.setTimeout(1, TimeUnit.SECONDS);
+
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // very expensive operation that typically finishes within 1 second but can take up to 5 seconds,
+ // simulated using sleep()
+ try {
+ Thread.sleep(5 * OPERATION_DURATION);
+ return "DONE";
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "INTERRUPTED";
+ } finally {
+ LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName());
+ }
+ }
+ }).start();
+ }
+
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(AsyncResource.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 asynchronous POST.
+ *
+ * Send 3 async POST requests and wait to receive the responses. Check the response content and
+ * assert that the operation did not take more than twice as long as a single long operation duration
+ * (this ensures async request execution).
+ *
+ * @throws Exception in case of a test error.
+ */
+ @Test
+ public void testAsyncPost() throws Exception {
+ final long tic = System.currentTimeMillis();
+
+ // Submit requests asynchronously.
+ final Future<Response> rf1 = target(PATH).request().async().post(Entity.text("1"));
+ final Future<Response> rf2 = target(PATH).request().async().post(Entity.text("2"));
+ final Future<Response> rf3 = target(PATH).request().async().post(Entity.text("3"));
+ // get() waits for the response
+
+ // workaround for AHC default connection manager limitation of
+ // only 2 open connections per host that may intermittently block
+ // the test
+ final CountDownLatch latch = new CountDownLatch(3);
+ ExecutorService executor = Executors.newFixedThreadPool(3);
+
+ final Future<String> r1 = executor.submit(new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ try {
+ return rf1.get().readEntity(String.class);
+ } finally {
+ latch.countDown();
+ }
+ }
+ });
+ final Future<String> r2 = executor.submit(new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ try {
+ return rf2.get().readEntity(String.class);
+ } finally {
+ latch.countDown();
+ }
+ }
+ });
+ final Future<String> r3 = executor.submit(new Callable<String>() {
+ @Override
+ public String call() throws Exception {
+ try {
+ return rf3.get().readEntity(String.class);
+ } finally {
+ latch.countDown();
+ }
+ }
+ });
+
+ assertTrue("Waiting for results has timed out.", latch.await(5 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS));
+ final long toc = System.currentTimeMillis();
+
+ assertEquals("DONE-1", r1.get());
+ assertEquals("DONE-2", r2.get());
+ assertEquals("DONE-3", r3.get());
+
+ final int asyncTimeoutMultiplier = getAsyncTimeoutMultiplier();
+ LOGGER.info("Using async timeout multiplier: " + asyncTimeoutMultiplier);
+ assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(4 * AsyncResource.OPERATION_DURATION
+ * asyncTimeoutMultiplier));
+
+ }
+
+ /**
+ * Test accessing an operation that times out on the server.
+ *
+ * @throws Exception in case of a test error.
+ */
+ @Test
+ public void testAsyncGetWithTimeout() throws Exception {
+ final Future<Response> responseFuture = target(PATH).path("timeout").request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+
+ // get() waits for the response
+ assertEquals(503, response.getStatus());
+ assertEquals("Operation time out.", response.readEntity(String.class));
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AuthTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AuthTest.java
new file mode 100644
index 0000000..f59c0cf
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AuthTest.java
@@ -0,0 +1,595 @@
+/*
+ * 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.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.WebApplicationException;
+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 javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import javax.inject.Singleton;
+
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.CredentialsStore;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
+import org.glassfish.jersey.client.authentication.ResponseAuthenticationException;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class AuthTest extends JerseyTest {
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(PreemptiveAuthResource.class, AuthResource.class);
+ }
+
+ @Path("/")
+ public static class PreemptiveAuthResource {
+
+ @GET
+ public String get(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ assertNotNull(value);
+ return "GET";
+ }
+
+ @POST
+ public String post(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ assertNotNull(value);
+ return e;
+ }
+ }
+
+ @Test
+ public void testPreemptiveAuth() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider)
+ .property(Apache5ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+
+ WebTarget r = client.target(getBaseUri());
+ assertEquals("GET", r.request().get(String.class));
+ }
+
+ @Test
+ public void testPreemptiveAuthPost() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider)
+ .property(Apache5ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+
+ WebTarget r = client.target(getBaseUri());
+ assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+ }
+
+ @Path("/test")
+ @Singleton
+ public static class AuthResource {
+
+ int requestCount = 0;
+ int queryParamsBasicRequestCount = 0;
+ int queryParamsDigestRequestCount = 0;
+
+ @GET
+ public String get(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return "GET";
+ }
+
+ @GET
+ @Path("filter")
+ public String getFilter(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return "GET";
+ }
+
+ @GET
+ @Path("basicAndDigest")
+ public String getBasicAndDigest(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
+ .header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
+ .entity("Forbidden").build());
+ } else if (value.startsWith("Basic")) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
+ .header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
+ .entity("Digest authentication expected").build());
+ }
+
+ return "GET";
+ }
+
+ @GET
+ @Path("noauth")
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("digest")
+ public String getDigest(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
+ .entity("Forbidden").build());
+ }
+
+ return "GET";
+ }
+
+ @POST
+ public String post(@Context HttpHeaders h, String e) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return e;
+ }
+
+ @POST
+ @Path("filter")
+ public String postFilter(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return e;
+ }
+
+ @DELETE
+ public void delete(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+ }
+
+ @DELETE
+ @Path("filter")
+ public void deleteFilter(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+ }
+
+ @DELETE
+ @Path("filter/withEntity")
+ public String deleteFilterWithEntity(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return e;
+ }
+
+ @GET
+ @Path("content")
+ public String getWithContent(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
+ .entity("Forbidden").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return "GET";
+ }
+
+ @GET
+ @Path("contentDigestAuth")
+ public String getWithContentDigestAuth(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Digest nonce=\"1234\"")
+ .entity("Forbidden").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return "GET";
+ }
+
+ @GET
+ @Path("queryParamsBasic")
+ public String getQueryParamsBasic(@Context HttpHeaders h, @Context UriInfo uriDetails) {
+ queryParamsBasicRequestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+ return "GET " + queryParamsBasicRequestCount;
+ }
+
+ @GET
+ @Path("queryParamsDigest")
+ public String getQueryParamsDigest(@Context HttpHeaders h, @Context UriInfo uriDetails) {
+ queryParamsDigestRequestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Digest realm=\"WallyWorld\"").build());
+ }
+ return "GET " + queryParamsDigestRequestCount;
+ }
+ }
+
+ @Test
+ public void testAuthGet() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri()).path("test");
+
+ assertEquals("GET", r.request().get(String.class));
+ }
+
+ @Test
+ public void testAuthGetWithRequestCredentialsProvider() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri()).path("test");
+
+ assertEquals("GET",
+ r.request()
+ .property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider)
+ .get(String.class));
+ }
+
+ @Test
+ public void testAuthGetWithClientFilter() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.basic("name", "password"));
+ WebTarget r = client.target(getBaseUri()).path("test/filter");
+
+ assertEquals("GET", r.request().get(String.class));
+ }
+
+ @Test
+ public void testAuthGetWithBasicAndDigestFilter() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.universal("name", "password"));
+ WebTarget r = client.target(getBaseUri()).path("test/basicAndDigest");
+
+ assertEquals("GET", r.request().get(String.class));
+ }
+
+ @Test
+ public void testAuthGetBasicNoChallenge() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.basicBuilder().build());
+ WebTarget r = client.target(getBaseUri()).path("test/noauth");
+
+ assertEquals("GET", r.request().get(String.class));
+ }
+
+ @Test
+ public void testAuthGetWithDigestFilter() {
+ ClientConfig cc = new ClientConfig();
+ PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ cc.property(Apache5ClientProperties.CONNECTION_MANAGER, cm);
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.universal("name", "password"));
+ WebTarget r = client.target(getBaseUri()).path("test/digest");
+
+ assertEquals("GET", r.request().get(String.class));
+
+ // Verify the connection that was used for the request is available for reuse
+ // and no connections are leased
+ assertEquals(cm.getTotalStats().getAvailable(), 1);
+ assertEquals(cm.getTotalStats().getLeased(), 0);
+ }
+
+ @Test
+ @Ignore("JERSEY-1750: Cannot retry request with a non-repeatable request entity. How to buffer the entity?"
+ + " Allow repeatable write in jersey?")
+ public void testAuthPost() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri()).path("test");
+
+ assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+ }
+
+ @Test
+ public void testAuthPostWithClientFilter() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.basic("name", "password"));
+ WebTarget r = client.target(getBaseUri()).path("test/filter");
+
+ assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+ }
+
+ @Test
+ public void testAuthDelete() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri()).path("test");
+
+ Response response = r.request().delete();
+ assertEquals(response.getStatus(), 204);
+ }
+
+ @Test
+ public void testAuthDeleteWithClientFilter() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.basic("name", "password"));
+ WebTarget r = client.target(getBaseUri()).path("test/filter");
+
+ Response response = r.request().delete();
+ assertEquals(204, response.getStatus());
+ }
+
+ @Test
+ public void testAuthInteractiveGet() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+
+ WebTarget r = client.target(getBaseUri()).path("test");
+
+ assertEquals("GET", r.request().get(String.class));
+ }
+
+ @Test
+ @Ignore("JERSEY-1750: Cannot retry request with a non-repeatable request entity. How to buffer the entity?"
+ + " Allow repeatable write in jersey?")
+ public void testAuthInteractivePost() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri()).path("test");
+
+ assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+ }
+
+ @Test
+ public void testAuthGetWithBasicFilterAndContent() {
+ ClientConfig cc = new ClientConfig();
+ PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ cc.property(Apache5ClientProperties.CONNECTION_MANAGER, cm);
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.universalBuilder().build());
+ WebTarget r = client.target(getBaseUri()).path("test/content");
+
+ try {
+ assertEquals("GET", r.request().get(String.class));
+ fail();
+ } catch (ResponseAuthenticationException ex) {
+ // expected
+ }
+
+ // Verify the connection that was used for the request is available for reuse
+ // and no connections are leased
+ assertEquals(cm.getTotalStats().getAvailable(), 1);
+ assertEquals(cm.getTotalStats().getLeased(), 0);
+ }
+
+ @Test
+ public void testAuthGetWithDigestFilterAndContent() {
+ ClientConfig cc = new ClientConfig();
+ PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ cc.property(Apache5ClientProperties.CONNECTION_MANAGER, cm);
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.universalBuilder().build());
+ WebTarget r = client.target(getBaseUri()).path("test/contentDigestAuth");
+
+ try {
+ assertEquals("GET", r.request().get(String.class));
+ fail();
+ } catch (ResponseAuthenticationException ex) {
+ // expected
+ }
+
+ // Verify the connection that was used for the request is available for reuse
+ // and no connections are leased
+ assertEquals(cm.getTotalStats().getAvailable(), 1);
+ assertEquals(cm.getTotalStats().getLeased(), 0);
+ }
+
+ @Test
+ public void testAuthGetQueryParamsBasic() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.universal("name", "password"));
+
+ WebTarget r = client.target(getBaseUri()).path("test/queryParamsBasic");
+ assertEquals("GET 2", r.request().get(String.class));
+
+ r = client.target(getBaseUri())
+ .path("test/queryParamsBasic")
+ .queryParam("param1", "value1")
+ .queryParam("param2", "value2");
+ assertEquals("GET 3", r.request().get(String.class));
+
+ }
+
+ @Test
+ public void testAuthGetQueryParamsDigest() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ client.register(HttpAuthenticationFeature.universal("name", "password"));
+
+ WebTarget r = client.target(getBaseUri()).path("test/queryParamsDigest");
+ assertEquals("GET 2", r.request().get(String.class));
+
+ r = client.target(getBaseUri())
+ .path("test/queryParamsDigest")
+ .queryParam("param1", "value1")
+ .queryParam("param2", "value2");
+ assertEquals("GET 3", r.request().get(String.class));
+ }
+
+ @Test
+ public void testAuthGetWithConfigurator() {
+ CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+ credentialsProvider.setCredentials(
+ new AuthScope("localhost", getPort()),
+ new UsernamePasswordCredentials("name", "password".toCharArray())
+ );
+ Apache5HttpClientBuilderConfigurator apache5HttpClientBuilderConfigurator = (httpClientBuilder) -> {
+ return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
+ };
+
+ ClientConfig cc = new ClientConfig();
+ cc.register(apache5HttpClientBuilderConfigurator);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri()).path("test");
+
+ assertEquals("GET", r.request().get(String.class));
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CookieTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CookieTest.java
new file mode 100644
index 0000000..0e619a6
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CookieTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Cookie;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.JerseyClientBuilder;
+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.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class CookieTest extends JerseyTest {
+
+ @Path("/")
+ public static class CookieResource {
+
+ @GET
+ public Response get(@Context HttpHeaders h) {
+ Cookie c = h.getCookies().get("name");
+ String e = (c == null) ? "NO-COOKIE" : c.getValue();
+ return Response.ok(e)
+ .cookie(new NewCookie("name", "value")).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(CookieResource.class);
+ }
+
+ @Test
+ public void testCookieResource() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("value", r.request().get(String.class));
+ }
+
+ @Test
+ public void testDisabledCookies() {
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.DISABLE_COOKIES, true);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ JerseyClient client = JerseyClientBuilder.createClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+
+ final Apache5Connector connector = (Apache5Connector) client.getConfiguration().getConnector();
+ if (connector.getCookieStore() != null) {
+ assertTrue(connector.getCookieStore().getCookies().isEmpty());
+ } else {
+ assertNull(connector.getCookieStore());
+ }
+ }
+
+ @Test
+ public void testCookies() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ JerseyClient client = JerseyClientBuilder.createClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("value", r.request().get(String.class));
+
+ final Apache5Connector connector = (Apache5Connector) client.getConfiguration().getConnector();
+ assertNotNull(connector.getCookieStore().getCookies());
+ assertEquals(1, connector.getCookieStore().getCookies().size());
+ assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue());
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CustomLoggingFilter.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CustomLoggingFilter.java
new file mode 100644
index 0000000..27caeda
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CustomLoggingFilter.java
@@ -0,0 +1,70 @@
+/*
+ * 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.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.ClientResponseContext;
+import javax.ws.rs.client.ClientResponseFilter;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Custom logging filter.
+ *
+ * @author Santiago Pericas-Geertsen (santiago.pericasgeertsen at oracle.com)
+ */
+public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter,
+ ClientRequestFilter, ClientResponseFilter {
+
+ static int preFilterCalled = 0;
+ static int postFilterCalled = 0;
+
+ @Override
+ public void filter(ClientRequestContext context) throws IOException {
+ System.out.println("CustomLoggingFilter.preFilter called");
+ assertEquals(context.getConfiguration().getProperty("foo"), "bar");
+ preFilterCalled++;
+ }
+
+ @Override
+ public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException {
+ System.out.println("CustomLoggingFilter.postFilter called");
+ assertEquals(context.getConfiguration().getProperty("foo"), "bar");
+ postFilterCalled++;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context) throws IOException {
+ System.out.println("CustomLoggingFilter.preFilter called");
+ assertEquals(context.getProperty("foo"), "bar");
+ preFilterCalled++;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException {
+ System.out.println("CustomLoggingFilter.postFilter called");
+ assertEquals(context.getProperty("foo"), "bar");
+ postFilterCalled++;
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/DisableContentEncodingTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/DisableContentEncodingTest.java
new file mode 100644
index 0000000..51bc6a0
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/DisableContentEncodingTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Ondrej Kosatka
+ */
+public class DisableContentEncodingTest extends JerseyTest {
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(Resource.class);
+ }
+
+ @Path("/")
+ public static class Resource {
+
+ @GET
+ public String get(@HeaderParam("Accept-Encoding") String enc) {
+ return enc;
+ }
+ }
+
+ @Test
+ public void testDisabledByRequestConfig() {
+ ClientConfig cc = new ClientConfig(GZipEncoder.class);
+ final RequestConfig requestConfig = RequestConfig.custom().setContentCompressionEnabled(false).build();
+ cc.property(Apache5ClientProperties.REQUEST_CONFIG, requestConfig);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ String enc = r.request().get().readEntity(String.class);
+ assertEquals("", enc);
+ }
+
+ @Test
+ public void testEnabledByRequestConfig() {
+ ClientConfig cc = new ClientConfig(GZipEncoder.class);
+ final RequestConfig requestConfig = RequestConfig.custom().setContentCompressionEnabled(true).build();
+ cc.property(Apache5ClientProperties.REQUEST_CONFIG, requestConfig);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ String enc = r.request().get().readEntity(String.class);
+ assertEquals("gzip, x-gzip, deflate", enc);
+ }
+
+ @Test
+ public void testDefaultEncoding() {
+ ClientConfig cc = new ClientConfig(GZipEncoder.class);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ String enc = r.request().get().readEntity(String.class);
+ assertEquals("gzip, x-gzip, deflate", enc);
+ }
+
+ @Test
+ public void testDefaultEncodingOverridden() {
+ ClientConfig cc = new ClientConfig(GZipEncoder.class);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ String enc = r.request().acceptEncoding("gzip").get().readEntity(String.class);
+ assertEquals("gzip", enc);
+ }
+
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/FollowRedirectsTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/FollowRedirectsTest.java
new file mode 100644
index 0000000..9377a0a
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/FollowRedirectsTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientResponseContext;
+import javax.ws.rs.client.ClientResponseFilter;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientResponse;
+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.junit.Assert.assertEquals;
+
+/**
+ * Apache connector follow redirect tests.
+ *
+ * @author Martin Matula
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Marek Potociar
+ */
+public class FollowRedirectsTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName());
+
+ @Path("/test")
+ public static class RedirectResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("redirect")
+ public Response redirect() {
+ return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(RedirectResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ private static class RedirectTestFilter implements ClientResponseFilter {
+ public static final String RESOLVED_URI_HEADER = "resolved-uri";
+
+ @Override
+ public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
+ if (responseContext instanceof ClientResponse) {
+ ClientResponse clientResponse = (ClientResponse) responseContext;
+ responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString());
+ }
+ }
+ }
+
+ @Test
+ public void testDoFollow() {
+ Response r = target("test/redirect").register(RedirectTestFilter.class).request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ assertEquals(
+ UriBuilder.fromUri(getBaseUri()).path(RedirectResource.class).build().toString(),
+ r.getHeaderString(RedirectTestFilter.RESOLVED_URI_HEADER));
+ }
+
+ @Test
+ public void testDontFollow() {
+ WebTarget t = target("test/redirect");
+ t.property(ClientProperties.FOLLOW_REDIRECTS, false);
+ assertEquals(303, t.request().get().getStatus());
+ }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/GZIPContentEncodingTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/GZIPContentEncodingTest.java
new file mode 100644
index 0000000..0230ba1
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/GZIPContentEncodingTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.Arrays;
+
+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.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class GZIPContentEncodingTest extends JerseyTest {
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(Resource.class);
+ }
+
+ @Path("/")
+ public static class Resource {
+
+ @POST
+ public byte[] post(byte[] content) {
+ return content;
+ }
+ }
+
+ @Test
+ public void testPost() {
+ ClientConfig cc = new ClientConfig(GZipEncoder.class);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ byte[] content = new byte[1024 * 1024];
+ assertTrue(Arrays.equals(content,
+ r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+ Response cr = r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPostChunked() {
+ ClientConfig cc = new ClientConfig(GZipEncoder.class);
+ cc.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+
+ WebTarget r = client.target(getBaseUri());
+
+ byte[] content = new byte[1024 * 1024];
+ assertTrue(Arrays.equals(content,
+ r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+ Response cr = r.request().post(Entity.text("POST"));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HelloWorldTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HelloWorldTest.java
new file mode 100644
index 0000000..e0f6612
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HelloWorldTest.java
@@ -0,0 +1,400 @@
+/*
+ * 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.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.InternalServerErrorException;
+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.InvocationCallback;
+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 org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.io.ConnectionEndpoint;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.io.LeaseRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Jakub Podlesak
+ */
+public class HelloWorldTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName());
+ private static final String ROOT_PATH = "helloworld";
+
+ @Path("helloworld")
+ public static class HelloWorldResource {
+
+ public static final String CLICHED_MESSAGE = "Hello World!";
+
+ @GET
+ @Produces("text/plain")
+ public String getHello() {
+ return CLICHED_MESSAGE;
+ }
+
+ @GET
+ @Produces("text/plain")
+ @Path("error")
+ public Response getError() {
+ return Response.serverError().entity("Error.").build();
+ }
+
+ @GET
+ @Produces("text/plain")
+ @Path("error2")
+ public Response getError2() {
+ return Response.serverError().entity("Error2.").build();
+ }
+
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HelloWorldResource.class);
+ config.register(new LoggingFeature(LOGGER, Level.INFO, LoggingFeature.Verbosity.PAYLOAD_ANY,
+ LoggingFeature.DEFAULT_MAX_ENTITY_SIZE));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new Apache5ConnectorProvider());
+ }
+
+ @Test
+ public void testConnection() {
+ Response response = target().path(ROOT_PATH).request("text/plain").get();
+ assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void testClientStringResponse() {
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ }
+
+ @Test
+ public void testConnectionPoolSharingEnabled() throws Exception {
+ _testConnectionPoolSharing(true);
+ }
+
+ @Test
+ public void testConnectionPoolSharingDisabled() throws Exception {
+ _testConnectionPoolSharing(false);
+ }
+
+ public void _testConnectionPoolSharing(final boolean sharingEnabled) throws Exception {
+
+ final HttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+
+ final ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager);
+ cc.property(Apache5ClientProperties.CONNECTION_MANAGER_SHARED, sharingEnabled);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+
+ final Client clientOne = ClientBuilder.newClient(cc);
+ WebTarget target = clientOne.target(getBaseUri()).path(ROOT_PATH);
+ target.request().get();
+ clientOne.close();
+
+ final boolean exceptionExpected = !sharingEnabled;
+
+ final Client clientTwo = ClientBuilder.newClient(cc);
+ target = clientTwo.target(getBaseUri()).path(ROOT_PATH);
+ try {
+ target.request().get();
+ if (exceptionExpected) {
+ Assert.fail("Exception expected");
+ }
+ } catch (Exception e) {
+ if (!exceptionExpected) {
+ Assert.fail("Exception not expected");
+ }
+ } finally {
+ clientTwo.close();
+ }
+
+ if (sharingEnabled) {
+ connectionManager.close();
+ }
+ }
+
+ @Test
+ public void testAsyncClientRequests() throws InterruptedException {
+ HttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+ ClientConfig cc = new ClientConfig();
+ cc.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager);
+ cc.connectorProvider(new Apache5ConnectorProvider());
+ Client client = ClientBuilder.newClient(cc);
+ WebTarget target = client.target(getBaseUri());
+ final int REQUESTS = 20;
+ final CountDownLatch latch = new CountDownLatch(REQUESTS);
+ final long tic = System.currentTimeMillis();
+ final Map<Integer, String> results = new ConcurrentHashMap<Integer, String>();
+ for (int i = 0; i < REQUESTS; i++) {
+ final int id = i;
+ target.path(ROOT_PATH).request().async().get(new InvocationCallback<Response>() {
+ @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.
+ * <p>
+ * 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 0000000..cad5ea8
--- /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 0000000..c63b7a8
--- /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<String> {
+
+ 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<String, Object> 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 0000000..00a45a1
--- /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 0000000..35b38c3
--- /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 0000000..40f1b41
--- /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.
+ * <p>
+ * In order to be able to inspect the request body, the generated data is a sequence of numbers
+ * delimited with new lines. Such as
+ * <pre><code>
+ * 1
+ * 2
+ * 3
+ *
+ * ...
+ *
+ * 57234
+ * 57235
+ * 57236
+ *
+ * ...
+ * </code></pre>
+ * 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 0000000..46ba4ee
--- /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.
+ * <p>
+ * On the client side, appends a new custom request header with a configured name and value to each outgoing request.
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @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.
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+ @Path("public")
+ public static class PublicResource {
+
+ @Uri("a")
+ @ClientA // resolves to <base>/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 0000000..cdea49b
--- /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 0000000..6254c43
--- /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 0000000..ce6a377
--- /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 0000000..ea90e79
--- /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<String> 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<String> 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 0000000..7eab1b3
--- /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 0000000..d95c2f2
--- /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<ContainerRequestContext, Response>() {
+
+ @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<String> 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<String, List<String>> headers) {
+ for (Map.Entry<String, List<String>> e : headers.entrySet()) {
+ List<String> 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 0000000..8248fdd
--- /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 0000000..0c2e320
--- /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 8266de6..18fce3c 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- Copyright (c) 2011, 2021 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2011, 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
@@ -35,6 +35,7 @@
<modules>
<module>apache-connector</module>
+ <module>apache5-connector</module>
<module>grizzly-connector</module>
<module>jdk-connector</module>
<module>jetty-connector</module>
diff --git a/pom.xml b/pom.xml
index 62bcb7b..8f3bb20 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1625,6 +1625,11 @@
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents.client5</groupId>
+ <artifactId>httpclient5</artifactId>
+ <version>${httpclient5.version}</version>
+ </dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
@@ -2125,6 +2130,7 @@
<hk2.jvnet.osgi.version>org.jvnet.hk2.*;version="[2.5,4)"</hk2.jvnet.osgi.version>
<hk2.config.version>5.1.0</hk2.config.version>
<httpclient.version>4.5.13</httpclient.version>
+ <httpclient5.version>5.1.2</httpclient5.version>
<jackson.version>2.13.0</jackson.version>
<jackson1.version>1.9.13</jackson1.version>
<javassist.version>3.25.0-GA</javassist.version>