diff --git a/connectors/apache5-connector/pom.xml b/connectors/apache5-connector/pom.xml new file mode 100644 index 0000000000..669ac11e79 --- /dev/null +++ b/connectors/apache5-connector/pom.xml @@ -0,0 +1,89 @@ + + + + + 4.0.0 + + + org.glassfish.jersey.connectors + project + 2.36-SNAPSHOT + + + jersey-apache5-connector + jar + jersey-connectors-apache5 + + Jersey Client Transport via Apache HttpClient 5.x + + + UTF-8 + + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.glassfish.jersey.containers + jersey-container-grizzly2-http + ${project.version} + test + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${project.version} + test + + + com.google.guava + guava + test + + + + + + + com.sun.istack + istack-commons-maven-plugin + true + + + org.codehaus.mojo + build-helper-maven-plugin + true + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.felix + maven-bundle-plugin + true + + + + + 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 0000000000..eac5be5f2f --- /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. + *

+ * The value MUST be an instance of {@link org.apache.hc.client5.http.auth.CredentialsProvider}. + *

+ * If the property is absent a default provider will be used. + *

+ * The name of the configuration property is {@value}. + */ + 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. + *

+ * The value MUST be an instance of {@link java.lang.Boolean}. + *

+ * The default value is {@code false}. + *

+ * The name of the configuration property is {@value}. + */ + 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. + *

+ * This property may only be set prior to constructing Apache connector using {@link Apache5ConnectorProvider}. + *

+ * The value MUST be an instance of {@link java.lang.Boolean}. + *

+ * The default value is {@code false}. + *

+ * The name of the configuration property is {@value}. + */ + 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}. + *

+ * The value MUST be an instance of {@link org.apache.hc.client5.http.io.HttpClientConnectionManager}. + *

+ * 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. + *

+ * The name of the configuration property is {@value}. + */ + 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. + *

+ * This property may only be set prior to constructing Apache connector using {@link Apache5ConnectorProvider}. + *

+ * The value MUST be an instance of {@link java.lang.Boolean}. + *

+ * The default value is {@code false}. + *

+ * The name of the configuration property is {@value}. + * + * @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}. + *

+ * The value MUST be an instance of {@link org.apache.hc.client5.http.config.RequestConfig}. + *

+ * If the property is absent default request configuration will be used. + *

+ * The name of the configuration property is {@value}. + * + * @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}. + *

+ * The value MUST be an instance of {@link org.apache.hc.client5.http.HttpRequestRetryStrategy}. + *

+ * If the property is absent a default retry handler will be used + * ({@link org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy}). + *

+ * The name of the configuration property is {@value}. + */ + public static final String RETRY_STRATEGY = "jersey.config.apache5.client.retryStrategy"; + + /** + * ConnectionReuseStrategy for the {@link org.apache.hc.client5.http.classic.HttpClient}. + *

+ * The value MUST be an instance of {@link org.apache.hc.core5.http.ConnectionReuseStrategy}. + *

+ * If the property is absent the default reuse strategy of the Apache HTTP library will be used + *

+ * The name of the configuration property is {@value}. + */ + public static final String REUSE_STRATEGY = "jersey.config.apache5.client.reuseStrategy"; + + /** + * ConnectionKeepAliveStrategy for the {@link org.apache.hc.client5.http.classic.HttpClient}. + *

+ * The value MUST be an instance of {@link org.apache.hc.client5.http.ConnectionKeepAliveStrategy}. + *

+ * If the property is absent the default keepalive strategy of the Apache HTTP library will be used + *

+ * The name of the configuration property is {@value}. + */ + 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}. + *

+ * The value MUST be an instance of {@link java.lang.Boolean}. + *

+ * The default value is {@code false}. + *

+ * The name of the configuration property is {@value}. + */ + 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 Type of the property value. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static T getValue(final Map properties, final String key, final Class 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 0000000000..ad23f2d53f --- /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: + *

+ *     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.
+ * 
+ * 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. + *

+ * 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 0000000000..13b827910f --- /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. + *

+ * The following properties are only supported at construction of this class: + *

+ *

+ * 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. + *

+ *

+ * 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}. + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * Client operations are thread safe, the HTTP connection may + * be shared between different threads. + *

+ *

+ * 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. + *

+ *

+ * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE. + *

+ * + * @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 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 registry = RegistryBuilder.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 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 redirectLocations = context.getRedirectLocations().getAll(); + if (redirectLocations != null && !redirectLocations.isEmpty()) { + responseContext.setResolvedRequestUri(redirectLocations.get(redirectLocations.size() - 1)); + } + + final Header[] respHeaders = response.getHeaders(); + final MultivaluedMap headers = responseContext.getHeaders(); + for (final Header header : respHeaders) { + final String headerName = header.getName(); + List 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 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 writeOutBoundHeaders(final ClientRequest clientRequest, + final HttpUriRequest request) { + final Map stringHeaders = + HeaderUtils.asStringHeadersSingleValue(clientRequest.getHeaders(), clientRequest.getConfiguration()); + + for (final Map.Entry 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 0000000000..9a32a2ce3c --- /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. + *

+ * The following connector configuration properties are supported: + *

    + *
  • {@link Apache5ClientProperties#CONNECTION_MANAGER}
  • + *
  • {@link Apache5ClientProperties#REQUEST_CONFIG}
  • + *
  • {@link Apache5ClientProperties#CREDENTIALS_PROVIDER}
  • + *
  • {@link Apache5ClientProperties#DISABLE_COOKIES}
  • + *
  • {@link Apache5ClientProperties#KEEPALIVE_STRATEGY}
  • + *
  • {@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}
  • + *
  • {@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}
  • + *
  • {@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}
  • + *
  • {@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING} + * - default value is {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED}
  • + *
  • {@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}
  • + *
  • {@link Apache5ClientProperties#RETRY_STRATEGY}
  • + *
  • {@link Apache5ClientProperties#REUSE_STRATEGY}
  • + *
+ *

+ *

+ * 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. + *

+ *

+ * 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}. + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE. + *

+ * + * @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 0000000000..d179f5c620 --- /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 0000000000..0b05d4b425 --- /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 0000000000..16dbc30eca --- /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 0000000000..50e8f6571f --- /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 rf1 = target(PATH).request().async().post(Entity.text("1")); + final Future rf2 = target(PATH).request().async().post(Entity.text("2")); + final Future 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 r1 = executor.submit(new Callable() { + @Override + public String call() throws Exception { + try { + return rf1.get().readEntity(String.class); + } finally { + latch.countDown(); + } + } + }); + final Future r2 = executor.submit(new Callable() { + @Override + public String call() throws Exception { + try { + return rf2.get().readEntity(String.class); + } finally { + latch.countDown(); + } + } + }); + final Future r3 = executor.submit(new Callable() { + @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 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 0000000000..f59c0cf9bf --- /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 0000000000..0e619a661a --- /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 0000000000..27caedad53 --- /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 0000000000..51bc6a007c --- /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 0000000000..9377a0a741 --- /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 0000000000..0230ba14e0 --- /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 0000000000..e0f6612177 --- /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 results = new ConcurrentHashMap(); + for (int i = 0; i < REQUESTS; i++) { + final int id = i; + target.path(ROOT_PATH).request().async().get(new InvocationCallback() { + @Override + public void completed(Response response) { + try { + final String result = response.readEntity(String.class); + results.put(id, result); + } finally { + latch.countDown(); + } + } + + @Override + public void failed(Throwable error) { + Logger.getLogger(HelloWorldTest.class.getName()).log(Level.SEVERE, "Failed on throwable", error); + results.put(id, "error: " + error.getMessage()); + latch.countDown(); + } + }); + } + assertTrue(latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS)); + final long toc = System.currentTimeMillis(); + Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic)); + + StringBuilder resultInfo = new StringBuilder("Results:\n"); + for (int i = 0; i < REQUESTS; i++) { + String result = results.get(i); + resultInfo.append(i).append(": ").append(result).append('\n'); + } + Logger.getLogger(HelloWorldTest.class.getName()).info(resultInfo.toString()); + + for (int i = 0; i < REQUESTS; i++) { + String result = results.get(i); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, result); + } + } + + @Test + public void testHead() { + Response response = target().path(ROOT_PATH).request().head(); + assertEquals(200, response.getStatus()); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + } + + @Test + public void testFooBarOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals("foo/bar", response.getMediaType().toString()); + assertEquals(0, response.getLength()); + } + + @Test + public void testTextPlainOptions() { + Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options(); + assertEquals(200, response.getStatus()); + final String allowHeader = response.getHeaderString("Allow"); + _checkAllowContent(allowHeader); + assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType()); + final String responseBody = response.readEntity(String.class); + _checkAllowContent(responseBody); + } + + private void _checkAllowContent(final String content) { + assertTrue(content.contains("GET")); + assertTrue(content.contains("HEAD")); + assertTrue(content.contains("OPTIONS")); + } + + @Test + public void testMissingResourceNotFound() { + Response response; + + response = target().path(ROOT_PATH + "arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + + response = target().path(ROOT_PATH).path("arbitrary").request().get(); + assertEquals(404, response.getStatus()); + response.close(); + } + + @Test + public void testLoggingFilterClientClass() { + Client client = client(); + client.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterClientInstance() { + Client client = client(); + client.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target().path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterTargetClass() { + WebTarget target = target().path(ROOT_PATH); + target.register(CustomLoggingFilter.class).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testLoggingFilterTargetInstance() { + WebTarget target = target().path(ROOT_PATH); + target.register(new CustomLoggingFilter()).property("foo", "bar"); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = target.request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + @Test + public void testConfigurationUpdate() { + Client client1 = client(); + client1.register(CustomLoggingFilter.class).property("foo", "bar"); + + Client client = ClientBuilder.newClient(client1.getConfiguration()); + CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0; + String s = client.target(getBaseUri()).path(ROOT_PATH).request().get(String.class); + assertEquals(HelloWorldResource.CLICHED_MESSAGE, s); + assertEquals(1, CustomLoggingFilter.preFilterCalled); + assertEquals(1, CustomLoggingFilter.postFilterCalled); + } + + /** + * JERSEY-2157 reproducer. + *

+ * The test ensures that entities of the error responses which cause + * WebApplicationException being thrown by a JAX-RS client are buffered + * and that the underlying input connections are automatically released + * in such case. + */ + @Test + public void testConnectionClosingOnExceptionsForErrorResponses() { + final BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager(); + final AtomicInteger connectionCounter = new AtomicInteger(0); + + final ClientConfig config = new ClientConfig().property(Apache5ClientProperties.CONNECTION_MANAGER, + new HttpClientConnectionManager() { + @Override + public LeaseRequest lease(String id, HttpRoute route, Timeout requestTimeout, Object state) { + connectionCounter.incrementAndGet(); + return cm.lease(id, route, requestTimeout, state); + } + + @Override + public void release(ConnectionEndpoint endpoint, Object newState, TimeValue validDuration) { + connectionCounter.decrementAndGet(); + cm.release(endpoint, newState, validDuration); + } + + @Override + public void connect( + ConnectionEndpoint endpoint, + TimeValue connectTimeout, + HttpContext context + ) throws IOException { + cm.connect(endpoint, connectTimeout, context); + } + + @Override + public void upgrade(ConnectionEndpoint endpoint, HttpContext context) throws IOException { + cm.upgrade(endpoint, context); + } + + @Override + public void close(CloseMode closeMode) { + cm.close(closeMode); + } + + @Override + public void close() throws IOException { + cm.close(); + } + }); + config.connectorProvider(new Apache5ConnectorProvider()); + + final Client client = ClientBuilder.newClient(config); + final WebTarget rootTarget = client.target(getBaseUri()).path(ROOT_PATH); + + // Test that connection is getting closed properly for error responses. + try { + final String response = rootTarget.path("error").request().get(String.class); + fail("Exception expected. Received: " + response); + } catch (InternalServerErrorException isee) { + // do nothing - connection should be closed properly by now + } + + // Fail if the previous connection has not been closed automatically. + assertEquals(0, connectionCounter.get()); + + try { + final String response = rootTarget.path("error2").request().get(String.class); + fail("Exception expected. Received: " + response); + } catch (InternalServerErrorException isee) { + assertEquals("Received unexpected data.", "Error2.", isee.getResponse().readEntity(String.class)); + // Test buffering: + // second read would fail if entity was not buffered + assertEquals("Unexpected data in the entity buffer.", "Error2.", isee.getResponse().readEntity(String.class)); + } + + assertEquals(0, connectionCounter.get()); + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java new file mode 100644 index 0000000000..cad5ea8494 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.InputStreamEntity; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Assert; +import org.junit.Test; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.util.logging.Logger; + +public class HttpEntityTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HttpEntityTest.class.getName()); + private static final String ECHO_MESSAGE = "ECHO MESSAGE"; + + @Path("/") + public static class Resource { + @POST + public String echo(String message) { + return message; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class) + .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + config.connectorProvider(new Apache5ConnectorProvider()); + } + + @Test + public void testInputStreamEntity() { + ByteArrayInputStream bais = new ByteArrayInputStream(ECHO_MESSAGE.getBytes()); + InputStreamEntity entity = new InputStreamEntity(bais, ContentType.TEXT_PLAIN); + + try (Response response = target().request().post(Entity.entity(entity, MediaType.APPLICATION_OCTET_STREAM))) { + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ECHO_MESSAGE, response.readEntity(String.class)); + } + } + + @Test + public void testByteArrayEntity() { + ByteArrayEntity entity = new ByteArrayEntity(ECHO_MESSAGE.getBytes(), ContentType.TEXT_PLAIN); + + try (Response response = target().request().post(Entity.entity(entity, MediaType.APPLICATION_OCTET_STREAM))) { + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ECHO_MESSAGE, response.readEntity(String.class)); + } + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java new file mode 100644 index 0000000000..c63b7a87ee --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.logging.Logger; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class HttpHeadersTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + + @POST + public String post( + @HeaderParam("Transfer-Encoding") String transferEncoding, + @HeaderParam("X-CLIENT") String xClient, + @HeaderParam("X-WRITER") String xWriter, + String entity) { + assertEquals("client", xClient); + if (transferEncoding == null || !transferEncoding.equals("chunked")) { + assertEquals("writer", xWriter); + } + return entity; + } + } + + @Provider + @Produces("text/plain") + public static class HeaderWriter implements MessageBodyWriter { + + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == String.class; + } + + public long getSize(String t, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + public void writeTo(String t, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) throws IOException, WebApplicationException { + httpHeaders.add("X-WRITER", "writer"); + entityStream.write(t.getBytes()); + } + } + + @Override + protected Application configure() { + enable(TestProperties.LOG_TRAFFIC); + enable(TestProperties.DUMP_ENTITY); + + ResourceConfig config = new ResourceConfig(HttpMethodResource.class, HeaderWriter.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.READ_TIMEOUT, 1000).connectorProvider(new Apache5ConnectorProvider()); + } + + @Test + public void testPost() { + WebTarget r = target("test"); + + Response cr = r.request().header("X-CLIENT", "client").post(Entity.text("POST")); + assertEquals(200, cr.getStatus()); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostChunked() { + WebTarget r = target("test"); + + Response cr = r.request().header("X-CLIENT", "client").post(Entity.text("POST")); + assertEquals(200, cr.getStatus()); + assertTrue(cr.hasEntity()); + cr.close(); + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java new file mode 100644 index 0000000000..00a45a15b6 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class HttpMethodTest extends JerseyTest { + + @Override + protected Application configure() { + return new ResourceConfig(HttpMethodResource.class, ErrorResource.class); + } + + protected Client createClient() { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new Apache5ConnectorProvider()); + return ClientBuilder.newClient(cc); + } + + protected Client createPoolingClient() { + ClientConfig cc = new ClientConfig(); + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(100); + connectionManager.setDefaultMaxPerRoute(100); + cc.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager); + cc.connectorProvider(new Apache5ConnectorProvider()); + return ClientBuilder.newClient(cc); + } + + private WebTarget getWebTarget(final Client client) { + return client.target(getBaseUri()).path("test"); + } + + private WebTarget getWebTarget() { + return getWebTarget(createClient()); + } + + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @HttpMethod("PATCH") + public @interface PATCH { + } + + @Path("/test") + public static class HttpMethodResource { + @GET + public String get() { + return "GET"; + } + + @POST + public String post(String entity) { + return entity; + } + + @PUT + public String put(String entity) { + return entity; + } + + @DELETE + public String delete() { + return "DELETE"; + } + + @DELETE + @Path("withentity") + public String delete(String entity) { + return entity; + } + + @POST + @Path("noproduce") + public void postNoProduce(String entity) { + } + + @POST + @Path("noconsumeproduce") + public void postNoConsumeProduce() { + } + + @PATCH + public String patch(String entity) { + return entity; + } + } + + @Test + public void testHead() { + WebTarget r = getWebTarget(); + Response cr = r.request().head(); + assertFalse(cr.hasEntity()); + } + + @Test + public void testOptions() { + WebTarget r = getWebTarget(); + Response cr = r.request().options(); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testOptionsWithEntity() { + WebTarget r = getWebTarget(); + Response response = r.request().build("OPTIONS", Entity.text("OPTIONS")).invoke(); + assertEquals(200, response.getStatus()); + response.close(); + } + + @Test + public void testGet() { + WebTarget r = getWebTarget(); + assertEquals("GET", r.request().get(String.class)); + + Response cr = r.request().get(); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPost() { + WebTarget r = getWebTarget(); + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + + Response cr = r.request().post(Entity.text("POST")); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostChunked() { + ClientConfig cc = new ClientConfig() + .property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024) + .connectorProvider(new Apache5ConnectorProvider()); + Client client = ClientBuilder.newClient(cc); + WebTarget r = getWebTarget(client); + + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + + Response cr = r.request().post(Entity.text("POST")); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPostVoid() { + WebTarget r = getWebTarget(createPoolingClient()); + + for (int i = 0; i < 100; i++) { + r.request().post(Entity.text("POST")); + } + } + + @Test + public void testPostNoProduce() { + WebTarget r = getWebTarget(); + assertEquals(204, r.path("noproduce").request().post(Entity.text("POST")).getStatus()); + + Response cr = r.path("noproduce").request().post(Entity.text("POST")); + assertFalse(cr.hasEntity()); + cr.close(); + } + + + @Test + public void testPostNoConsumeProduce() { + WebTarget r = getWebTarget(); + assertEquals(204, r.path("noconsumeproduce").request().post(null).getStatus()); + + Response cr = r.path("noconsumeproduce").request().post(Entity.text("POST")); + assertFalse(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPut() { + WebTarget r = getWebTarget(); + assertEquals("PUT", r.request().put(Entity.text("PUT"), String.class)); + + Response cr = r.request().put(Entity.text("PUT")); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testDelete() { + WebTarget r = getWebTarget(); + assertEquals("DELETE", r.request().delete(String.class)); + + Response cr = r.request().delete(); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testPatch() { + WebTarget r = getWebTarget(); + assertEquals("PATCH", r.request().method("PATCH", Entity.text("PATCH"), String.class)); + + Response cr = r.request().method("PATCH", Entity.text("PATCH")); + assertTrue(cr.hasEntity()); + cr.close(); + } + + @Test + public void testAll() { + WebTarget r = getWebTarget(); + + assertEquals("GET", r.request().get(String.class)); + + assertEquals("POST", r.request().post(Entity.text("POST"), String.class)); + + assertEquals(204, r.path("noproduce").request().post(Entity.text("POST")).getStatus()); + + assertEquals(204, r.path("noconsumeproduce").request().post(null).getStatus()); + + assertEquals("PUT", r.request().post(Entity.text("PUT"), String.class)); + + assertEquals("DELETE", r.request().delete(String.class)); + } + + + @Path("/error") + public static class ErrorResource { + @POST + public Response post(String entity) { + return Response.serverError().build(); + } + + @Path("entity") + @POST + public Response postWithEntity(String entity) { + return Response.serverError().entity("error").build(); + } + } + + @Test + public void testPostError() { + WebTarget r = createClient().target(getBaseUri()).path("error"); + + for (int i = 0; i < 100; i++) { + try { + final Response post = r.request().post(Entity.text("POST")); + post.close(); + } catch (ClientErrorException ex) { + } + } + } + + @Test + public void testPostErrorWithEntity() { + WebTarget r = createPoolingClient().target(getBaseUri()).path("error/entity"); + + for (int i = 0; i < 100; i++) { + try { + r.request().post(Entity.text("POST")); + } catch (ClientErrorException ex) { + String s = ex.getResponse().readEntity(String.class); + assertEquals("error", s); + } + } + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java new file mode 100644 index 0000000000..35b38c3bdc --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class HttpMethodWithClientFilterTest extends HttpMethodTest { + + @Override + protected Client createClient() { + ClientConfig cc = new ClientConfig() + .register(LoggingFeature.class) + .connectorProvider(new Apache5ConnectorProvider()); + return ClientBuilder.newClient(cc); + } + +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java new file mode 100644 index 0000000000..40f1b4136d --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Logger; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.ServerErrorException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.StreamingOutput; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Assert; +import org.junit.Test; + +/** + * The LargeDataTest reproduces a problem when bytes of large data sent are incorrectly sent. + * As a result, the request body is different than what was sent by the client. + *

+ * In order to be able to inspect the request body, the generated data is a sequence of numbers + * delimited with new lines. Such as + *


+ *     1
+ *     2
+ *     3
+ *
+ *     ...
+ *
+ *     57234
+ *     57235
+ *     57236
+ *
+ *     ...
+ * 
+ * It is also possible to send the data to netcat: {@code nc -l 8080} and verify the problem is + * on the client side. + * + * @author Stepan Vavra + * @author Marek Potociar + */ +public class LargeDataTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(LargeDataTest.class.getName()); + private static final int LONG_DATA_SIZE = 1_000_000; // for large set around 5GB, try e.g.: 536_870_912; + private static volatile Throwable exception; + + private static StreamingOutput longData(long sequence) { + return out -> { + long offset = 0; + while (offset < sequence) { + out.write(Long.toString(offset).getBytes()); + out.write('\n'); + offset++; + } + }; + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new Apache5ConnectorProvider()); + } + + @Test + public void postWithLargeData() throws Throwable { + WebTarget webTarget = target("test"); + + Response response = webTarget.request().post(Entity.entity(longData(LONG_DATA_SIZE), MediaType.TEXT_PLAIN_TYPE)); + + try { + if (exception != null) { + + // the reason to throw the exception is that IntelliJ gives you an option to compare the expected with the actual + throw exception; + } + + Assert.assertEquals("Unexpected error: " + response.getStatus(), + Status.Family.SUCCESSFUL, + response.getStatusInfo().getFamily()); + } finally { + response.close(); + } + } + + @Path("/test") + public static class HttpMethodResource { + + @POST + public Response post(InputStream content) { + try { + + longData(LONG_DATA_SIZE).write(new OutputStream() { + + private long position = 0; +// private long mbRead = 0; + + @Override + public void write(final int generated) throws IOException { + int received = content.read(); + + if (received != generated) { + throw new IOException("Bytes don't match at position " + position + + ": received=" + received + + ", generated=" + generated); + } + + position++; +// if (position % (1024 * 1024) == 0) { +// mbRead++; +// System.out.println("MB read: " + mbRead); +// } + } + }); + } catch (IOException e) { + exception = e; + throw new ServerErrorException(e.getMessage(), 500, e); + } + + return Response.ok().build(); + } + + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java new file mode 100644 index 0000000000..46ba4ee018 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ClientBinding; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.Uri; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * Jersey programmatic managed client test + * + * @author Marek Potociar + */ +public class ManagedClientTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName()); + + /** + * Managed client configuration for client A. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + @ClientBinding(configClass = MyClientAConfig.class) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public static @interface ClientA { + } + + /** + * Managed client configuration for client B. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + @ClientBinding(configClass = MyClientBConfig.class) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public @interface ClientB { + } + + /** + * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance + * to every method that is annotated with {@link Require @Require} internal feature + * annotation. + * + * @author Marek Potociar + */ + public static class CustomHeaderFeature implements DynamicFeature { + + /** + * A method annotation to be placed on those resource methods to which a validating + * {@link CustomHeaderFilter} instance should be added. + */ + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Target(ElementType.METHOD) + public static @interface Require { + + /** + * Expected custom header name to be validated by the {@link CustomHeaderFilter}. + */ + public String headerName(); + + /** + * Expected custom header value to be validated by the {@link CustomHeaderFilter}. + */ + public String headerValue(); + } + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class); + if (va != null) { + context.register(new CustomHeaderFilter(va.headerName(), va.headerValue())); + } + } + } + + /** + * A filter for appending and validating custom headers. + *

+ * On the client side, appends a new custom request header with a configured name and value to each outgoing request. + *

+ *

+ * On the server side, validates that each request has a custom header with a configured name and value. + * If the validation fails a HTTP 403 response is returned. + *

+ * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter { + + private final String headerName; + private final String headerValue; + + public CustomHeaderFilter(String headerName, String headerValue) { + if (headerName == null || headerValue == null) { + throw new IllegalArgumentException("Header name and value must not be null."); + } + this.headerName = headerName; + this.headerValue = headerValue; + } + + @Override + public void filter(ContainerRequestContext ctx) throws IOException { // validate + if (!headerValue.equals(ctx.getHeaderString(headerName))) { + ctx.abortWith(Response.status(Response.Status.FORBIDDEN) + .type(MediaType.TEXT_PLAIN) + .entity(String + .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue)) + .build()); + } + } + + @Override + public void filter(ClientRequestContext ctx) throws IOException { // append + ctx.getHeaders().putSingle(headerName, headerValue); + } + } + + /** + * Internal resource accessed from the managed client resource. + * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + @Path("internal") + public static class InternalResource { + + @GET + @Path("a") + @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a") + public String getA() { + return "a"; + } + + @GET + @Path("b") + @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b") + public String getB() { + return "b"; + } + } + + /** + * A resource that uses managed clients to retrieve values of internal + * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter} + * and require a specific custom header in a request to be set to a specific value. + *

+ * Properly configured managed clients have a {@code CustomHeaderFilter} instance + * configured to insert the {@link CustomHeaderFeature.Require required} custom header + * with a proper value into the outgoing client requests. + *

+ * + * @author Marek Potociar (marek.potociar at oracle.com) + */ + @Path("public") + public static class PublicResource { + + @Uri("a") + @ClientA // resolves to /internal/a + private WebTarget targetA; + + @GET + @Produces("text/plain") + @Path("a") + public String getTargetA() { + return targetA.request(MediaType.TEXT_PLAIN).get(String.class); + } + + @GET + @Produces("text/plain") + @Path("b") + public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) { + return targetB.request(MediaType.TEXT_PLAIN).get(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class) + .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal"); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + public static class MyClientAConfig extends ClientConfig { + + public MyClientAConfig() { + this.register(new CustomHeaderFilter("custom-header", "a")); + } + } + + public static class MyClientBConfig extends ClientConfig { + + public MyClientBConfig() { + this.register(new CustomHeaderFilter("custom-header", "b")); + } + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new Apache5ConnectorProvider()); + } + + /** + * Test that a connection via managed clients works properly. + * + * @throws Exception in case of test failure. + */ + @Test + public void testManagedClient() throws Exception { + final WebTarget resource = target().path("public").path("{name}"); + Response response; + + response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get(); + assertEquals(200, response.getStatus()); + assertEquals("a", response.readEntity(String.class)); + + response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get(); + assertEquals(200, response.getStatus()); + assertEquals("b", response.readEntity(String.class)); + } + +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java new file mode 100644 index 0000000000..cdea49be4c --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; + +/** + * @author Paul Sandoz + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class NoEntityTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName()); + + @Path("/test") + public static class HttpMethodResource { + @GET + public Response get() { + return Response.status(Status.CONFLICT).build(); + } + + @POST + public void post(String entity) { + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(HttpMethodResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new Apache5ConnectorProvider()); + } + + @Test + public void testGet() { + WebTarget r = target("test"); + + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testGetWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().get(); + cr.close(); + } + } + + @Test + public void testPost() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + } + } + + @Test + public void testPostWithClose() { + WebTarget r = target("test"); + for (int i = 0; i < 5; i++) { + Response cr = r.request().post(null); + cr.close(); + } + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java new file mode 100644 index 0000000000..6254c430fc --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.io.IOException; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; + +import org.apache.hc.client5.http.HttpRequestRetryStrategy; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.TimeValue; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class RetryStrategyTest extends JerseyTest { + private static final int READ_TIMEOUT_MS = 100; + + @Override + protected Application configure() { + return new ResourceConfig(RetryHandlerResource.class); + } + + @Path("/") + public static class RetryHandlerResource { + private static volatile int postRequestNumber = 0; + private static volatile int getRequestNumber = 0; + + // Cause a timeout on the first GET and POST request + @GET + public String get(@Context HttpHeaders h) { + if (getRequestNumber++ == 0) { + try { + Thread.sleep(READ_TIMEOUT_MS * 10); + } catch (InterruptedException ex) { + // ignore + } + } + return "GET"; + } + + @POST + public String post(@Context HttpHeaders h, String e) { + if (postRequestNumber++ == 0) { + try { + Thread.sleep(READ_TIMEOUT_MS * 10); + } catch (InterruptedException ex) { + // ignore + } + } + return "POST"; + } + } + + @Test + public void testRetryGet() throws IOException { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new Apache5ConnectorProvider()); + cc.property(Apache5ClientProperties.RETRY_STRATEGY, + new HttpRequestRetryStrategy() { + @Override + public boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context) { + return true; + } + + @Override + public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) { + return true; + } + + @Override + public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) { + return TimeValue.ofMilliseconds(200); + } + }); + cc.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS); + Client client = ClientBuilder.newClient(cc); + + WebTarget r = client.target(getBaseUri()); + assertEquals("GET", r.request().get(String.class)); + } + + @Test + public void testRetryPost() throws IOException { + ClientConfig cc = new ClientConfig(); + cc.connectorProvider(new Apache5ConnectorProvider()); + cc.property(Apache5ClientProperties.RETRY_STRATEGY, + new HttpRequestRetryStrategy() { + @Override + public boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context) { + return true; + } + + @Override + public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) { + return true; + } + + @Override + public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) { + return TimeValue.ofMilliseconds(200); + } + }); + cc.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS); + Client client = ClientBuilder.newClient(cc); + + WebTarget r = client.target(getBaseUri()); + assertEquals("POST", r.request() + .property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED) + .post(Entity.text("POST"), String.class)); + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java new file mode 100644 index 0000000000..ce6a377883 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.message.GZipEncoder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +/** + * + * @author Miroslav Fuksa + */ +public class SpecialHeaderTest extends JerseyTest { + @Override + protected Application configure() { + return new ResourceConfig(MyResource.class, GZipEncoder.class, LoggingFeature.class); + } + + @Path("resource") + public static class MyResource { + @GET + @Produces("text/plain") + @Path("encoded") + public Response getEncoded() { + return Response.ok("get").header(HttpHeaders.CONTENT_ENCODING, "gzip").build(); + } + + @GET + @Produces("text/plain") + @Path("non-encoded") + public Response getNormal() { + return Response.ok("get").build(); + } + } + + @Override + protected void configureClient(ClientConfig config) { + config.connectorProvider(new Apache5ConnectorProvider()); + } + + + @Test + @Ignore("Apache connector does not provide information about encoding for gzip and deflate encoding") + public void testEncoded() { + final Response response = target().path("resource/encoded").request("text/plain").get(); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals("get", response.readEntity(String.class)); + Assert.assertEquals("gzip", response.getHeaderString(HttpHeaders.CONTENT_ENCODING)); + Assert.assertEquals("text/plain", response.getHeaderString(HttpHeaders.CONTENT_TYPE)); + Assert.assertEquals(3, response.getHeaderString(HttpHeaders.CONTENT_LENGTH)); + } + + @Test + public void testNonEncoded() { + final Response response = target().path("resource/non-encoded").request("text/plain").get(); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals("get", response.readEntity(String.class)); + Assert.assertNull(response.getHeaderString(HttpHeaders.CONTENT_ENCODING)); + Assert.assertEquals("text/plain", response.getHeaderString(HttpHeaders.CONTENT_TYPE)); + Assert.assertEquals("3", response.getHeaderString(HttpHeaders.CONTENT_LENGTH)); + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java new file mode 100644 index 0000000000..ea90e79879 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.inject.Singleton; + +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.server.ChunkedOutput; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Petr Janouch + */ +public class StreamingTest extends JerseyTest { + private PoolingHttpClientConnectionManager connectionManager; + + /** + * Test that a data stream can be terminated from the client side. + */ + @Test + public void clientCloseNoTimeoutTest() throws IOException { + clientCloseTest(-1); + } + + @Test + public void clientCloseWithTimeOutTest() throws IOException { + clientCloseTest(1_000); + } + + /** + * Tests that closing a response after completely reading the entity reuses the connection + */ + @Test + public void reuseConnectionTest() throws IOException { + Response response = target().path("/streamingEndpoint/get").request().get(); + InputStream is = response.readEntity(InputStream.class); + byte[] buf = new byte[8192]; + is.read(buf); + is.close(); + response.close(); + + assertEquals(1, connectionManager.getTotalStats().getAvailable()); + assertEquals(0, connectionManager.getTotalStats().getLeased()); + } + + /** + * Tests that closing a request without reading the entity does not throw an exception. + */ + @Test + public void clientCloseThrowsNoExceptionTest() throws IOException { + Response response = target().path("/streamingEndpoint/get").request().get(); + response.close(); + } + + @Override + protected void configureClient(ClientConfig config) { + connectionManager = new PoolingHttpClientConnectionManager(); + config.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager); + config.connectorProvider(new Apache5ConnectorProvider()); + } + + @Override + protected Application configure() { + return new ResourceConfig(StreamingEndpoint.class); + } + + /** + * Test that a data stream can be terminated from the client side. + */ + private void clientCloseTest(int readTimeout) throws IOException { + // start streaming + AtomicInteger counter = new AtomicInteger(0); + Invocation.Builder builder = target().path("/streamingEndpoint").request(); + if (readTimeout > -1) { + counter.set(1); + builder.property(ClientProperties.READ_TIMEOUT, readTimeout); + builder.property(Apache5ClientProperties.CONNECTION_CLOSING_STRATEGY, + (Apache5ConnectionClosingStrategy) (config, request, response, stream) -> { + try { + stream.close(); + } catch (Exception e) { + // timeout, no chunk ending + } finally { + counter.set(0); + response.close(); + } + }); + } + InputStream inputStream = builder.get(InputStream.class); + + WebTarget sendTarget = target().path("/streamingEndpoint/send"); + // trigger sending 'A' to the stream; OK is sent if everything on the server was OK + assertEquals("OK", sendTarget.request().get().readEntity(String.class)); + // check 'A' has been sent + assertEquals('A', inputStream.read()); + // closing the stream should tear down the connection + inputStream.close(); + // trigger sending another 'A' to the stream; it should fail + // (indicating that the streaming has been terminated on the server) + assertEquals("NOK", sendTarget.request().get().readEntity(String.class)); + assertEquals(0, counter.get()); + } + + @Singleton + @Path("streamingEndpoint") + public static class StreamingEndpoint { + + private final ChunkedOutput output = new ChunkedOutput<>(String.class); + + @GET + @Path("send") + public String sendEvent() { + try { + output.write("A"); + } catch (IOException e) { + return "NOK"; + } + + return "OK"; + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + public ChunkedOutput get() { + return output; + } + + @GET + @Path("get") + @Produces(MediaType.TEXT_PLAIN) + public String getString() { + return "OK"; + } + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java new file mode 100644 index 0000000000..7eab1b30b6 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.net.SocketTimeoutException; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * @author Martin Matula + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +public class TimeoutTest extends JerseyTest { + private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName()); + + @Path("/test") + public static class TimeoutResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("timeout") + public String getTimeout() { + try { + Thread.sleep(2000); + } catch (final InterruptedException e) { + e.printStackTrace(); + } + return "GET"; + } + } + + @Override + protected Application configure() { + final ResourceConfig config = new ResourceConfig(TimeoutResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(final ClientConfig config) { + config.property(ClientProperties.READ_TIMEOUT, 1000); + config.connectorProvider(new Apache5ConnectorProvider()); + } + + @Test + public void testFast() { + final Response r = target("test").request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testSlow() { + try { + target("test/timeout").request().get(); + fail("Timeout expected."); + } catch (final ProcessingException e) { + assertThat("Unexpected processing exception cause", + e.getCause(), instanceOf(SocketTimeoutException.class)); + } + } + + @Test + public void testPerRequestTimeout() { + final Response r = target("test/timeout").request() + .property(ClientProperties.READ_TIMEOUT, 3000).get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java new file mode 100644 index 0000000000..d95c2f21c8 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.process.Inflector; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.test.JerseyTest; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * This very basic resource showcases support of a HTTP TRACE method, + * not directly supported by JAX-RS API. + * + * @author Marek Potociar + */ +public class TraceSupportTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName()); + + /** + * Programmatic tracing root resource path. + */ + public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic"; + + /** + * Annotated class-based tracing root resource path. + */ + public static final String ROOT_PATH_ANNOTATED = "tracing/annotated"; + + @HttpMethod(TRACE.NAME) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface TRACE { + public static final String NAME = "TRACE"; + } + + @Path(ROOT_PATH_ANNOTATED) + public static class TracingResource { + + @TRACE + @Produces("text/plain") + public String trace(Request request) { + return stringify((ContainerRequest) request); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(TracingResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC); + resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector() { + + @Override + public Response apply(ContainerRequestContext request) { + if (request == null) { + return Response.noContent().build(); + } else { + return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build(); + } + } + }); + + return config.registerResources(resourceBuilder.build()); + + } + + private String[] expectedFragmentsProgrammatic = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic" + }; + private String[] expectedFragmentsAnnotated = new String[]{ + "TRACE http://localhost:" + this.getPort() + "/tracing/annotated" + }; + + private WebTarget prepareTarget(String path) { + final WebTarget target = target(); + target.register(LoggingFeature.class); + return target.path(path); + } + + @Test + public void testProgrammaticApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsProgrammatic) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testAnnotatedApp() throws Exception { + Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode()); + + String responseEntity = response.readEntity(String.class); + for (String expectedFragment : expectedFragmentsAnnotated) { + assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity, + // toLowerCase - http header field names are case insensitive + responseEntity.contains(expectedFragment)); + } + } + + @Test + public void testTraceWithEntity() throws Exception { + _testTraceWithEntity(false, false); + } + + @Test + public void testAsyncTraceWithEntity() throws Exception { + _testTraceWithEntity(true, false); + } + + @Test + public void testTraceWithEntityApacheConnector() throws Exception { + _testTraceWithEntity(false, true); + } + + @Test + public void testAsyncTraceWithEntityApacheConnector() throws Exception { + _testTraceWithEntity(true, true); + } + + private void _testTraceWithEntity(final boolean isAsync, final boolean useApacheConnection) throws Exception { + try { + WebTarget target = useApacheConnection ? getApacheClient().target(target().getUri()) : target(); + target = target.path(ROOT_PATH_ANNOTATED); + + final Entity entity = Entity.entity("trace", MediaType.WILDCARD_TYPE); + + Response response; + if (!isAsync) { + response = target.request().method(TRACE.NAME, entity); + } else { + response = target.request().async().method(TRACE.NAME, entity).get(); + } + + fail("A TRACE request MUST NOT include an entity. (response=" + response + ")"); + } catch (Exception e) { + // OK + } + } + + private Client getApacheClient() { + return ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider())); + } + + + public static String stringify(ContainerRequest request) { + StringBuilder buffer = new StringBuilder(); + + printRequestLine(buffer, request); + printPrefixedHeaders(buffer, request.getHeaders()); + + if (request.hasEntity()) { + buffer.append(request.readEntity(String.class)).append("\n"); + } + + return buffer.toString(); + } + + private static void printRequestLine(StringBuilder buffer, ContainerRequest request) { + buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n"); + } + + private static void printPrefixedHeaders(StringBuilder buffer, Map> headers) { + for (Map.Entry> e : headers.entrySet()) { + List val = e.getValue(); + String header = e.getKey(); + + if (val.size() == 1) { + buffer.append(header).append(": ").append(val.get(0)).append("\n"); + } else { + StringBuilder sb = new StringBuilder(); + boolean add = false; + for (String s : val) { + if (add) { + sb.append(','); + } + add = true; + sb.append(s); + } + buffer.append(header).append(": ").append(sb.toString()).append("\n"); + } + } + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java new file mode 100644 index 0000000000..8248fdd7d8 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.apache.hc.client5.http.cookie.CookieStore; +import org.glassfish.jersey.client.ClientConfig; + +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +/** + * Test of access to the underlying CookieStore instance used by the connector. + * + * @author Maksim Mukosey (mmukosey at gmail.com) + */ +public class UnderlyingCookieStoreAccessTest { + + @Test + public void testCookieStoreInstanceAccess() { + final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider())); + final CookieStore csOnClient = Apache5ConnectorProvider.getCookieStore(client); + // important: the web target instance in this test must be only created AFTER the client has been pre-initialized + // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the + // connector provider's static getCookieStore method above. + final WebTarget target = client.target("http://localhost/"); + final CookieStore csOnTarget = Apache5ConnectorProvider.getCookieStore(target); + + assertNotNull("CookieStore instance set on JerseyClient should not be null.", csOnClient); + assertNotNull("CookieStore instance set on JerseyWebTarget should not be null.", csOnTarget); + assertSame("CookieStore instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget" + + "(provided the target instance has not been further configured).", csOnClient, csOnTarget); + } +} diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java new file mode 100644 index 0000000000..0c2e320b62 --- /dev/null +++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.apache5.connector; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; + +import org.apache.hc.client5.http.classic.HttpClient; +import org.glassfish.jersey.client.ClientConfig; + +import org.junit.Test; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +/** + * Test of access to the underlying HTTP client instance used by the connector. + * + * @author Marek Potociar + */ +public class UnderlyingHttpClientAccessTest { + + /** + * Verifier of JERSEY-2424 fix. + */ + @Test + public void testHttpClientInstanceAccess() { + final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider())); + final HttpClient hcOnClient = Apache5ConnectorProvider.getHttpClient(client); + // important: the web target instance in this test must be only created AFTER the client has been pre-initialized + // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the + // connector provider's static getHttpClient method above. + final WebTarget target = client.target("http://localhost/"); + final HttpClient hcOnTarget = Apache5ConnectorProvider.getHttpClient(target); + + assertNotNull("HTTP client instance set on JerseyClient should not be null.", hcOnClient); + assertNotNull("HTTP client instance set on JerseyWebTarget should not be null.", hcOnTarget); + assertSame("HTTP client instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget" + + "(provided the target instance has not been further configured).", + hcOnClient, hcOnTarget + ); + } +} diff --git a/connectors/pom.xml b/connectors/pom.xml index 8266de6355..18fce3cd15 100644 --- a/connectors/pom.xml +++ b/connectors/pom.xml @@ -1,7 +1,7 @@