Merge remote-tracking branch 'origin/2.x' into 'origin/3.0' Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
diff --git a/NOTICE.md b/NOTICE.md index b245c38..0388c31 100644 --- a/NOTICE.md +++ b/NOTICE.md
@@ -70,7 +70,7 @@ * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. -Jackson JAX-RS Providers Version 2.18.0 +Jackson JAX-RS Providers Version 2.19.1 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2024 FasterXML, LLC. All rights reserved unless otherwise indicated.
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java index 471321f..6072a52 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025 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 @@ -30,20 +30,21 @@ class Expect100ContinueConnectorExtension implements ConnectorExtension<HttpRequest, IOException> { + + private final NettyConnectorProvider.Config.RW requestConfiguration; + + Expect100ContinueConnectorExtension(NettyConnectorProvider.Config.RW requestConfiguration) { + this.requestConfiguration = requestConfiguration; + } + private static final String EXCEPTION_MESSAGE = "Server rejected operation"; @Override public void invoke(ClientRequest request, HttpRequest extensionParam) { final long length = request.getLengthLong(); - final RequestEntityProcessing entityProcessing = request.resolveProperty( - ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); - - final Boolean expectContinueActivated = request.resolveProperty( - ClientProperties.EXPECT_100_CONTINUE, Boolean.class); - final Long expectContinueSizeThreshold = request.resolveProperty( - ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, - ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE); - + final RequestEntityProcessing entityProcessing = requestConfiguration.requestEntityProcessing(request); + final Boolean expectContinueActivated = requestConfiguration.expect100Continue(request); + final long expectContinueSizeThreshold = requestConfiguration.expect100ContinueThreshold(request); final boolean allowStreaming = length > expectContinueSizeThreshold || entityProcessing == RequestEntityProcessing.CHUNKED;
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java index 7f5bb9b..1efebc2 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
@@ -31,7 +31,6 @@ import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; -import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.http.HttpHeaders; @@ -56,17 +55,14 @@ */ class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> { - private static final int DEFAULT_MAX_REDIRECTS = 5; - // Modified only by the same thread. No need to synchronize it. private final Set<URI> redirectUriHistory; private final ClientRequest jerseyRequest; private final CompletableFuture<ClientResponse> responseAvailable; private final CompletableFuture<?> responseDone; - private final boolean followRedirects; - private final int maxRedirects; private final NettyConnector connector; private final NettyHttpRedirectController redirectController; + private final NettyConnectorProvider.Config.RW requestConfiguration; private NettyInputStream nis; private ClientResponse jerseyResponse; @@ -74,19 +70,20 @@ private boolean readTimedOut; JerseyClientHandler(ClientRequest request, CompletableFuture<ClientResponse> responseAvailable, - CompletableFuture<?> responseDone, Set<URI> redirectUriHistory, NettyConnector connector) { + CompletableFuture<?> responseDone, Set<URI> redirectUriHistory, NettyConnector connector, + NettyConnectorProvider.Config.RW requestConfiguration) { this.redirectUriHistory = redirectUriHistory; this.jerseyRequest = request; this.responseAvailable = responseAvailable; this.responseDone = responseDone; - // Follow redirects by default - this.followRedirects = jerseyRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true); - this.maxRedirects = jerseyRequest.resolveProperty(NettyClientProperties.MAX_REDIRECTS, DEFAULT_MAX_REDIRECTS); + this.requestConfiguration = requestConfiguration; this.connector = connector; + // Follow redirects by default + requestConfiguration.followRedirects(jerseyRequest); + requestConfiguration.maxRedirects(jerseyRequest); - final NettyHttpRedirectController customRedirectController = jerseyRequest - .resolveProperty(NettyClientProperties.HTTP_REDIRECT_CONTROLLER, NettyHttpRedirectController.class); - this.redirectController = customRedirectController == null ? new NettyHttpRedirectController() : customRedirectController; + this.redirectController = requestConfiguration.redirectController(jerseyRequest); + this.redirectController.init(requestConfiguration); } @Override @@ -112,7 +109,7 @@ ClientResponse cr = jerseyResponse; jerseyResponse = null; int responseStatus = cr.getStatus(); - if (followRedirects + if (Boolean.TRUE.equals(requestConfiguration.followRedirects()) && (responseStatus == ResponseStatus.Redirect3xx.MOVED_PERMANENTLY_301.getStatusCode() || responseStatus == ResponseStatus.Redirect3xx.FOUND_302.getStatusCode() || responseStatus == ResponseStatus.Redirect3xx.SEE_OTHER_303.getStatusCode() @@ -139,16 +136,17 @@ // infinite loop detection responseAvailable.completeExceptionally( new RedirectException(LocalizationMessages.REDIRECT_INFINITE_LOOP())); - } else if (redirectUriHistory.size() > maxRedirects) { + } else if (redirectUriHistory.size() > requestConfiguration.maxRedirects.get()) { // maximal number of redirection - responseAvailable.completeExceptionally( - new RedirectException(LocalizationMessages.REDIRECT_LIMIT_REACHED(maxRedirects))); + responseAvailable.completeExceptionally(new RedirectException( + LocalizationMessages.REDIRECT_LIMIT_REACHED(requestConfiguration.maxRedirects.get()))); } else { ClientRequest newReq = new ClientRequest(jerseyRequest); newReq.setUri(newUri); ctx.close(); if (redirectController.prepareRedirect(newReq, cr)) { - final NettyConnector newConnector = new NettyConnector(newReq.getClient()); + final NettyConnector newConnector = + new NettyConnector(newReq.getClient(), connector.clientConfiguration); newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() { @Override public boolean complete(ClientResponse value) {
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java index 4b2e4c9..7dc3495 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java
@@ -73,6 +73,11 @@ public static final String IDLE_CONNECTION_PRUNE_TIMEOUT = "jersey.config.client.idleConnectionPruneTimeout"; /** + * Enable or disable the Netty logging by {@code LoggingHandler(Level.DEBUG)}. Disabled by default. + */ + public static final String LOGGING_ENABLED = "jersey.config.client.netty.loggingEnabled"; + + /** * <p> * This property determines the maximum number of idle connections that will be simultaneously kept alive, per destination. * The default is 5. @@ -157,7 +162,8 @@ DEFAULT_HEADER_SIZE = 8192; /** - * Parameter which allows extending of the initial line length for the Netty connector + * Parameter which allows extending of the first line length of the HTTP header for the Netty connector. + * Taken from {@link io.netty.handler.codec.http.HttpClientCodec#HttpClientCodec(int, int, int)}. * * @since 2.44 */ @@ -166,12 +172,12 @@ /** * Default initial line length for Netty Connector. - * Taken from {@link io.netty.handler.codec.http.HttpClientCodec#HttpClientCodec(int, int, int)} + * Typically, set this to the same value as {@link #MAX_HEADER_SIZE}. * * @since 2.44 */ public static final Integer - DEFAULT_INITIAL_LINE_LENGTH = 4096; + DEFAULT_INITIAL_LINE_LENGTH = 8192; /** * Parameter which allows extending of the chunk size for the Netty connector
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java new file mode 100644 index 0000000..5d8725c --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java
@@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 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.netty.connector; + +import org.glassfish.jersey.client.ClientRequest; + +import java.net.URI; + +/** + * Adjustable connection pooling controller. + */ +public class NettyConnectionController { + /** + * Get the group of connections to be pooled, purged idle, and reused together. + * + * @param clientRequest the HTTP client request. + * @param uri the uri for the HTTP client request. + * @param hostName the hostname for the request. Can differ from the hostname in the uri based on other request attributes. + * @param port the real port for the request. Can differ from the port in the uri based on other request attributes. + * @return the group of connections identifier. + */ + public String getConnectionGroup(ClientRequest clientRequest, URI uri, String hostName, int port) { + return uri.getScheme() + "://" + hostName + ":" + port; + } +}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java index a65560c..ba80967 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -18,8 +18,6 @@ import java.io.IOException; import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.SocketAddress; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; @@ -36,7 +34,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; import javax.net.ssl.SSLContext; @@ -56,10 +53,8 @@ import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpChunkedInput; -import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; @@ -68,8 +63,8 @@ import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; -import io.netty.handler.proxy.HttpProxyHandler; -import io.netty.handler.proxy.ProxyHandler; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.ApplicationProtocolConfig; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.IdentityCipherSuiteFilter; @@ -81,7 +76,6 @@ import io.netty.handler.timeout.IdleStateHandler; import io.netty.resolver.NoopAddressResolverGroup; import io.netty.util.concurrent.GenericFutureListener; -import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.client.innate.ClientProxy; @@ -106,6 +100,7 @@ final EventLoopGroup group; final Client client; final HashMap<String, ArrayList<Channel>> connections = new HashMap<>(); + final NettyConnectorProvider.Config.RW clientConfiguration; private static final LazyValue<String> NETTY_VERSION = Values.lazy( (Value<String>) () -> { @@ -118,63 +113,29 @@ return "Netty " + nettyVersion; }); - // If HTTP keepalive is enabled the value of "http.maxConnections" determines the maximum number - // of idle connections that will be simultaneously kept alive, per destination. - private static final String HTTP_KEEPALIVE_STRING = System.getProperty("http.keepAlive"); - // http.keepalive (default: true) - private static final Boolean HTTP_KEEPALIVE = - HTTP_KEEPALIVE_STRING == null ? Boolean.TRUE : Boolean.parseBoolean(HTTP_KEEPALIVE_STRING); - - // http.maxConnections (default: 5) - private static final int DEFAULT_MAX_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = Integer.getInteger("http.maxConnections", DEFAULT_MAX_POOL_SIZE); - private static final int DEFAULT_MAX_POOL_IDLE = 60; // seconds - private static final int DEFAULT_MAX_POOL_SIZE_TOTAL = 60; // connections - - - private final Integer maxPoolSize; // either from system property, or from Jersey config, or default - private final Integer maxPoolSizeTotal; //either from Jersey config, or default - private final Integer maxPoolIdle; // either from Jersey config, or default - static final String INACTIVE_POOLED_CONNECTION_HANDLER = "inactive_pooled_connection_handler"; private static final String PRUNE_INACTIVE_POOL = "prune_inactive_pool"; private static final String READ_TIMEOUT_HANDLER = "read_timeout_handler"; private static final String REQUEST_HANDLER = "request_handler"; private static final String EXPECT_100_CONTINUE_HANDLER = "expect_100_continue_handler"; - NettyConnector(Client client) { + NettyConnector(Client client, NettyConnectorProvider.Config.RW connectorConfiguration) { + this.client = client; + this.clientConfiguration = connectorConfiguration.fromClient(client); final Configuration configuration = client.getConfiguration(); - final Map<String, Object> properties = configuration.getProperties(); - final Object threadPoolSize = properties.get(ClientProperties.ASYNC_THREADPOOL_SIZE); - - if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) { - executorService = VirtualThreadUtil.withConfig(configuration).newFixedThreadPool((Integer) threadPoolSize); - this.group = new NioEventLoopGroup((Integer) threadPoolSize); + final Integer threadPoolSize = this.clientConfiguration.asyncThreadPoolSize(); + if (threadPoolSize != null && threadPoolSize > 0) { + executorService = VirtualThreadUtil + .withConfig(clientConfiguration.prefixedConfiguration(configuration)) + .newFixedThreadPool(threadPoolSize); + this.group = new NioEventLoopGroup(threadPoolSize); } else { - executorService = VirtualThreadUtil.withConfig(configuration).newCachedThreadPool(); + executorService = VirtualThreadUtil + .withConfig(clientConfiguration.prefixedConfiguration(configuration)) + .newCachedThreadPool(); this.group = new NioEventLoopGroup(); } - - this.client = client; - - final Object maxPoolSizeTotalProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS_TOTAL); - final Object maxPoolIdleProperty = properties.get(NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT); - final Object maxPoolSizeProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS); - - maxPoolSizeTotal = maxPoolSizeTotalProperty != null ? (Integer) maxPoolSizeTotalProperty : DEFAULT_MAX_POOL_SIZE_TOTAL; - maxPoolIdle = maxPoolIdleProperty != null ? (Integer) maxPoolIdleProperty : DEFAULT_MAX_POOL_IDLE; - maxPoolSize = maxPoolSizeProperty != null - ? (Integer) maxPoolSizeProperty - : (HTTP_KEEPALIVE ? MAX_POOL_SIZE : DEFAULT_MAX_POOL_SIZE); - - if (maxPoolSizeTotal < 0) { - throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_TOTAL(maxPoolSizeTotal)); - } - - if (maxPoolSize < 0) { - throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_SIZE(maxPoolSize)); - } } @Override @@ -207,25 +168,31 @@ protected void execute(final ClientRequest jerseyRequest, final Set<URI> redirectUriHistory, final CompletableFuture<ClientResponse> responseAvailable) { - Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0); - final Integer expect100ContinueTimeout = jerseyRequest.resolveProperty( - NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, - NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE); - if (timeout == null || timeout < 0) { - throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout)); + final NettyConnectorProvider.Config.RW requestConfiguration = + clientConfiguration + .fromRequest(jerseyRequest) + .readTimeout(jerseyRequest) + .expect100ContinueTimeout(jerseyRequest); + final int readTimeout = requestConfiguration.readTimeout(); + if (readTimeout < 0) { + throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(readTimeout)); } final CompletableFuture<?> responseDone = new CompletableFuture<>(); final URI requestUri = jerseyRequest.getUri(); - String host = requestUri.getHost(); - int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80; + final String host = requestUri.getHost(); + final int port = requestUri.getPort() != -1 + ? requestUri.getPort() + : "https".equalsIgnoreCase(requestUri.getScheme()) ? 443 : 80; try { - final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder() + final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder(requestConfiguration) .request(jerseyRequest).setSNIAlways(true).setSNIHostName(jerseyRequest).build(); - String key = requestUri.getScheme() + "://" + sslConfig.getSNIHostName() + ":" + port; + final String key = requestConfiguration + .connectionController() + .getConnectionGroup(jerseyRequest, requestUri, sslConfig.getSNIHostName(), port); ArrayList<Channel> conns; synchronized (connections) { conns = connections.get(key); @@ -245,8 +212,8 @@ } catch (NoSuchElementException e) { /* * Eat it. - * It could happen that the channel was closed, pipeline cleared and - * then it will fail to remove the names with this exception. + * It could happen that the channel was closed, pipeline cleared, + * and then it will fail to remove the names with this exception. */ } if (!chan.isOpen()) { @@ -258,20 +225,15 @@ final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler(); if (chan == null) { - Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0); + requestConfiguration.connectTimeout(jerseyRequest); Bootstrap b = new Bootstrap(); // http proxy - Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest(jerseyRequest); - if (!proxy.isPresent()) { - proxy = ClientProxy.proxyFromProperties(requestUri); - } - proxy.ifPresent(clientProxy -> { + final Optional<ClientProxy> handlerProxy = requestConfiguration.proxy(jerseyRequest, requestUri); + handlerProxy.ifPresent(clientProxy -> { b.resolver(NoopAddressResolverGroup.INSTANCE); // request hostname resolved by the HTTP proxy }); - final Optional<ClientProxy> handlerProxy = proxy; - b.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @@ -283,19 +245,14 @@ // http proxy handlerProxy.ifPresent(clientProxy -> { - final URI u = clientProxy.uri(); - InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(), - u.getPort() == -1 ? 8080 : u.getPort()); - ProxyHandler proxy1 = createProxyHandler(jerseyRequest, proxyAddr, - clientProxy.userName(), clientProxy.password(), connectTimeout); - p.addLast(proxy1); + p.addLast(requestConfiguration.createProxyHandler(clientProxy, jerseyRequest)); }); // Enable HTTPS if necessary. if ("https".equals(requestUri.getScheme())) { // making client authentication optional for now; it could be extracted to configurable property JdkSslContext jdkSslContext = new JdkSslContext( - getSslContext(client, jerseyRequest), + requestConfiguration.sslContext(client, jerseyRequest), true, (Iterable) null, IdentityCipherSuiteFilter.INSTANCE, @@ -310,8 +267,7 @@ final SslHandler sslHandler = jdkSslContext.newHandler( ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService ); - if (ClientProperties.getValue(config.getProperties(), - NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true)) { + if (requestConfiguration.isSslHostnameVerificationEnabled(config.getProperties())) { sslConfig.setEndpointIdentificationAlgorithm(sslHandler.engine()); } @@ -320,16 +276,10 @@ p.addLast(sslHandler); } - final Integer maxHeaderSize = ClientProperties.getValue(config.getProperties(), - NettyClientProperties.MAX_HEADER_SIZE, - NettyClientProperties.DEFAULT_HEADER_SIZE); - final Integer maxChunkSize = ClientProperties.getValue(config.getProperties(), - NettyClientProperties.MAX_CHUNK_SIZE, - NettyClientProperties.DEFAULT_CHUNK_SIZE); - final Integer maxInitialLineLength = ClientProperties.getValue(config.getProperties(), - NettyClientProperties.MAX_INITIAL_LINE_LENGTH, - NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH); - p.addLast(new HttpClientCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize)); + if (requestConfiguration.loggingEnabled.get()) { + p.addLast(new LoggingHandler(LogLevel.INFO)); + } + p.addLast(requestConfiguration.createHttpClientCodec(config.getProperties())); p.addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler); p.addLast(new ChunkedWriteHandler()); p.addLast(new HttpContentDecompressor()); @@ -337,8 +287,8 @@ }); // connect timeout - if (connectTimeout > 0) { - b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout); + if (requestConfiguration.connectTimeout() > 0) { + b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, requestConfiguration.connectTimeout()); } // Make the connection attempt. @@ -357,12 +307,12 @@ // assert: it is ok to abort the entire response, if responseDone is completed exceptionally - in particular, nothing // will leak final Channel ch = chan; - JerseyClientHandler clientHandler = - new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this); + JerseyClientHandler clientHandler = new JerseyClientHandler( + jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this, requestConfiguration); // read timeout makes sense really as an inactivity timeout ch.pipeline().addLast(READ_TIMEOUT_HANDLER, - new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS)); + new IdleStateHandler(0, 0, requestConfiguration.readTimeout(), TimeUnit.MILLISECONDS)); ch.pipeline().addLast(REQUEST_HANDLER, clientHandler); responseDone.whenComplete((_r, th) -> { @@ -370,7 +320,8 @@ ch.pipeline().remove(clientHandler); if (th == null) { - ch.pipeline().addLast(INACTIVE_POOLED_CONNECTION_HANDLER, new IdleStateHandler(0, 0, maxPoolIdle)); + ch.pipeline().addLast(INACTIVE_POOLED_CONNECTION_HANDLER, + new IdleStateHandler(0, 0, requestConfiguration.maxPoolIdle.get())); ch.pipeline().addLast(PRUNE_INACTIVE_POOL, new PruneIdlePool(connections, key)); boolean added = true; synchronized (connections) { @@ -381,7 +332,9 @@ connections.put(key, conns1); } else { synchronized (conns1) { - if ((maxPoolSizeTotal == 0 || connections.size() < maxPoolSizeTotal) && conns1.size() < maxPoolSize) { + if ((requestConfiguration.maxPoolSizeTotal.get() == 0 + || connections.size() < requestConfiguration.maxPoolSizeTotal.get()) + && conns1.size() < requestConfiguration.maxPoolSize.get()) { conns1.add(ch); } else { // else do not add the Channel to the idle pool added = false; @@ -433,7 +386,7 @@ }; ch.closeFuture().addListener(closeListener); - final NettyEntityWriter entityWriter = nettyEntityWriter(jerseyRequest, ch); + final NettyEntityWriter entityWriter = nettyEntityWriter(jerseyRequest, ch, requestConfiguration); switch (entityWriter.getType()) { case CHUNKED: HttpUtil.setTransferEncodingChunked(nettyRequest, true); @@ -493,7 +446,7 @@ }); headersSet.await(); - new Expect100ContinueConnectorExtension().invoke(jerseyRequest, nettyRequest); + new Expect100ContinueConnectorExtension(requestConfiguration).invoke(jerseyRequest, nettyRequest); boolean continueExpected = HttpUtil.is100ContinueExpected(nettyRequest); boolean expectationsFailed = false; @@ -503,7 +456,7 @@ expect100ContinueHandler.attachCountDownLatch(expect100ContinueLatch); //send expect request, sync and wait till either response or timeout received entityWriter.writeAndFlush(nettyRequest); - expect100ContinueLatch.await(expect100ContinueTimeout, TimeUnit.MILLISECONDS); + expect100ContinueLatch.await(requestConfiguration.expect100ContTimeout.get(), TimeUnit.MILLISECONDS); try { expect100ContinueHandler.processExpectationStatus(); } catch (TimeoutException e) { @@ -543,13 +496,10 @@ } } - /* package */ NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) { - return NettyEntityWriter.getInstance(clientRequest, channel); - } - - private SSLContext getSslContext(Client client, ClientRequest request) { - Supplier<SSLContext> supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class); - return supplier == null ? client.getSslContext() : supplier.get(); + /* package */ NettyEntityWriter nettyEntityWriter( + ClientRequest clientRequest, Channel channel, NettyConnectorProvider.Config.RW requestConfiguration) { + return NettyEntityWriter + .getInstance(clientRequest, channel, () -> requestConfiguration.requestEntityProcessing(clientRequest)); } private String buildPathWithQueryParameters(URI requestUri) { @@ -602,21 +552,7 @@ } } - private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr, - String userName, String password, long connectTimeout) { - final Boolean filter = jerseyRequest.resolveProperty(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, Boolean.TRUE); - HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filter)); - - ProxyHandler proxy = userName == null ? new HttpProxyHandler(proxyAddr, httpHeaders) - : new HttpProxyHandler(proxyAddr, userName, password, httpHeaders); - if (connectTimeout > 0) { - proxy.setConnectTimeoutMillis(connectTimeout); - } - - return proxy; - } - - private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) { + /* package */ static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) { for (final Map.Entry<String, List<String>> e : jerseyRequest.getStringHeaders().entrySet()) { final String key = e.getKey(); if (!proxyOnly || JerseyClientHandler.ProxyHeaders.INSTANCE.test(key) || additionalProxyHeadersToKeep(key)) {
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java new file mode 100644 index 0000000..99f763c --- /dev/null +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java
@@ -0,0 +1,503 @@ +/* + * Copyright (c) 2025 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.netty.connector; + +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.proxy.HttpProxyHandler; +import io.netty.handler.proxy.ProxyHandler; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.innate.ClientProxy; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.client.innate.ConnectorConfiguration; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Map; + +class NettyConnectorConfiguration<N extends NettyConnectorConfiguration<N>> extends ConnectorConfiguration<N> { + + /* package */ final NullableRef<NettyConnectionController> connectionController = NullableRef.empty(); + /* package */ final NullableRef<Boolean> enableHostnameVerification = NullableRef.empty(); + /* package */ final Ref<Integer> expect100ContTimeout = NullableRef.empty(); + /* package */ final NullableRef<Boolean> filterHeadersForProxy = NullableRef.empty(); + /* package */ final NullableRef<Integer> firstHttpHeaderLineLength = NullableRef.empty(); + /* package */ final Ref<Boolean> loggingEnabled = NullableRef.empty(); + /* package */ final NullableRef<Integer> maxChunkSize = NullableRef.empty(); + /* package */ final NullableRef<Integer> maxHeaderSize = NullableRef.empty(); + // either from Jersey config, or default + /* package */ final Ref<Integer> maxPoolSizeTotal = NullableRef.empty(); + // either from Jersey config, or default + /* package */ final Ref<Integer> maxPoolIdle = NullableRef.empty(); + // either from system property, or from Jersey config, or default + /* package */ final Ref<Integer> maxPoolSize = NullableRef.empty(); + /* package */ final Ref<Integer> maxRedirects = NullableRef.empty(); + /* package */ final NullableRef<Boolean> preserveMethodOnRedirect = NullableRef.empty(); + /* package */ final NullableRef<NettyHttpRedirectController> redirectController = NullableRef.empty(); + + // If HTTP keepalive is enabled the value of "http.maxConnections" determines the maximum number + // of idle connections that will be simultaneously kept alive, per destination. + private static final String HTTP_KEEPALIVE_STRING = System.getProperty("http.keepAlive"); + // http.keepalive (default: true) + private static final Boolean HTTP_KEEPALIVE = + HTTP_KEEPALIVE_STRING == null ? Boolean.TRUE : Boolean.parseBoolean(HTTP_KEEPALIVE_STRING); + + // http.maxConnections (default: 5) + private static final int DEFAULT_MAX_POOL_SIZE = 5; + private static final int MAX_POOL_SIZE = Integer.getInteger("http.maxConnections", DEFAULT_MAX_POOL_SIZE); + private static final int DEFAULT_MAX_POOL_IDLE = 60; // seconds + private static final int DEFAULT_MAX_POOL_SIZE_TOTAL = 60; // connections + + private static final int DEFAULT_MAX_REDIRECTS = 5; + + /** + * Set the connection pooling controller for the Netty Connector. + * + * @param controller the connection pooling controller. + * @return updated configuration. + */ + public N connectionController(NettyConnectionController controller) { + connectionController.set(controller); + return self(); + } + + /** + * This setting determines waiting time in milliseconds for 100-Continue response when 100-Continue is sent by the client. + * The property {@link NettyClientProperties#EXPECT_100_CONTINUE_TIMEOUT} has precedence over this setting. + * + * @param millis the timeout for 100-Continue response. + * @return updated configuration. + */ + public N expect100ContinueTimeout(int millis) { + expect100ContTimeout.set(millis); + return self(); + } + + /** + * Enable or disable the endpoint identification algorithm to HTTPS. The property + * {@link NettyClientProperties#ENABLE_SSL_HOSTNAME_VERIFICATION} takes precedence over this setting. + * + * @param enable enable or disable the hostname verification. + * @return updated configuration. + */ + public N enableSslHostnameVerification(boolean enable) { + enableHostnameVerification.set(enable); + return self(); + } + + /** + * Enable or disable the Netty logging by {@code LoggingHandler(Level.DEBUG)}. Disabled by default. + * The property {@link NettyClientProperties#LOGGING_ENABLED} takes precedence over this setting. + * + * @param enable to enable or disable. + * @return updated configuration. + */ + public N enableLoggingHandler(boolean enable) { + loggingEnabled.set(enable); + return self(); + } + + /** + * Filter the HTTP headers for requests (CONNECT) towards the proxy except for PROXY-prefixed + * and HOST headers when {@code true}. + * The property {@link NettyClientProperties#FILTER_HEADERS_FOR_PROXY} has precedence over this setting. + * + * @param filter to filter or not. The default is {@code true}. + * @return updated configuration. + */ + public N filterHeadersForProxy(boolean filter) { + filterHeadersForProxy.set(filter); + return self(); + } + + /** + * This property determines the number of seconds the idle connections are kept in the pool before pruned. + * The default is 60. Specify 0 to disable. The property {@link NettyClientProperties#IDLE_CONNECTION_PRUNE_TIMEOUT} + * has precedence over this setting. + * + * @param seconds the timeout in seconds. + * @return updated configuration. + */ + public N idleConnectionPruneTimeout(int seconds) { + maxPoolIdle.set(seconds); + return self(); + } + + /** + * Set the maximum length of the first line of the HTTP header. + * The property {@link NettyClientProperties#MAX_INITIAL_LINE_LENGTH} has precedence over this setting. + * + * @param length the length of the first line of the HTTP header. + * @return updated configuration. + */ + public N initialHttpHeaderLineLength(int length) { + firstHttpHeaderLineLength.set(length); + return self(); + } + + /** + * Set the maximum chunk size for the Netty connector. The property {@link NettyClientProperties#MAX_CHUNK_SIZE} + * has precedence over this setting. + * + * @param size the new size of chunks. + * @return updated configuration. + */ + public N maxChunkSize(int size) { + maxChunkSize.set(size); + return self(); + } + + /** + * This setting determines the maximum number of idle connections that will be simultaneously kept alive, per destination. + * The default is 5. The property {@link NettyClientProperties#MAX_CONNECTIONS} takes precedence over this setting. + * + * @param maxCount maximum number of idle connections per destination. + * @return updated configuration. + */ + public N maxConnectionsPerDestination(int maxCount) { + maxPoolSize.set(maxCount); + return self(); + } + + /** + * Set the maximum header size in bytes for the HTTP headers processed by Netty. + * The property {@link NettyClientProperties#MAX_HEADER_SIZE} has precedence over this setting. + * + * @param size the new maximum header size. + * @return updated configuration. + */ + public N maxHeaderSize(int size) { + maxHeaderSize.set(size); + return self(); + } + + /** + * Set the maximum number of redirects to prevent infinite redirect loop. The default is 5. + * The property {@link NettyClientProperties#MAX_REDIRECTS} has precedence over this setting. + * + * @param max the maximum number of redirects. + * @return updated configuration. + */ + public N maxRedirects(int max) { + maxRedirects.set(max); + return self(); + } + + /** + * Set the maximum number of idle connections that will be simultaneously kept alive. The property + * {@link NettyClientProperties#MAX_CONNECTIONS_TOTAL} has precedence over this setting. + * + * @param max the maximum number of idle connections. + * @return updated configuration. + */ + public N maxTotalConnections(int max) { + maxPoolSizeTotal.set(max); + return self(); + } + + /** + * Set the preservation of methods during HTTP redirect. + * By default, the HTTP POST request are not transformed into HTTP GET for status 301 & 302. + * The property {@link NettyClientProperties#PRESERVE_METHOD_ON_REDIRECT} has precedence over this setting. + * + * @param preserve to preserve or not to preserve. + * @return updated configuration. + */ + public N preserveMethodOnRedirect(boolean preserve) { + preserveMethodOnRedirect.set(preserve); + return self(); + } + + /** + * Set the Netty Connector HTTP redirect controller. + * The property {@link NettyClientProperties#HTTP_REDIRECT_CONTROLLER} has precedence over this setting. + * + * @param controller the HTTP redirect controller. + * @return updated configuration. + */ + public N redirectController(NettyHttpRedirectController controller) { + redirectController.set(controller); + return self(); + } + + @SuppressWarnings("unchecked") + protected N self() { + return (N) this; + } + + abstract static class ReadWrite<N extends ReadWrite<N>> + extends NettyConnectorConfiguration<N> + implements ConnectorConfiguration.Read<N> { + + @Override + public <X extends ConnectorConfiguration<?>> void setNonEmpty(X otherCC) { + NettyConnectorConfiguration<?> other = (NettyConnectorConfiguration<?>) otherCC; + ConnectorConfiguration.Read.super.setNonEmpty(other); + this.connectionController.setNonEmpty(other.connectionController); + this.redirectController.setNonEmpty(other.redirectController); + this.connectionController.setNonEmpty(other.connectionController); + this.enableHostnameVerification.setNonEmpty(other.enableHostnameVerification); + ((NullableRef<Integer>) this.expect100ContTimeout).setNonEmpty((NullableRef<Integer>) other.expect100ContTimeout); + this.filterHeadersForProxy.setNonEmpty(other.filterHeadersForProxy); + this.firstHttpHeaderLineLength.setNonEmpty(other.firstHttpHeaderLineLength); + ((NullableRef<Boolean>) this.loggingEnabled).setNonEmpty((NullableRef<Boolean>) other.loggingEnabled); + this.maxChunkSize.setNonEmpty(other.maxChunkSize); + this.maxHeaderSize.setNonEmpty(other.maxHeaderSize); + ((NullableRef<Integer>) this.maxPoolIdle).setNonEmpty((NullableRef<Integer>) other.maxPoolIdle); + ((NullableRef<Integer>) this.maxPoolSize).setNonEmpty((NullableRef<Integer>) other.maxPoolSize); + ((NullableRef<Integer>) this.maxPoolSizeTotal).setNonEmpty((NullableRef<Integer>) other.maxPoolSizeTotal); + ((NullableRef<Integer>) this.maxRedirects).setNonEmpty((NullableRef<Integer>) other.maxRedirects); + this.preserveMethodOnRedirect.setNonEmpty(other.preserveMethodOnRedirect); + this.redirectController.setNonEmpty(other.redirectController); + } + + @Override + public N init() { + Read.super.init(); + enableSslHostnameVerification(Boolean.TRUE); + expect100ContinueTimeout(NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE); + filterHeadersForProxy(Boolean.TRUE); + initialHttpHeaderLineLength(NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH); + enableLoggingHandler(Boolean.FALSE); + maxChunkSize(NettyClientProperties.DEFAULT_CHUNK_SIZE); + maxHeaderSize(NettyClientProperties.DEFAULT_HEADER_SIZE); + // either from Jersey config, or default + maxTotalConnections(DEFAULT_MAX_POOL_SIZE_TOTAL); + // either from Jersey config, or default + maxPoolIdle.set(DEFAULT_MAX_POOL_IDLE); + // either from system property, or from Jersey config, or default + maxPoolSize.set(HTTP_KEEPALIVE ? MAX_POOL_SIZE : DEFAULT_MAX_POOL_SIZE); + maxRedirects(DEFAULT_MAX_REDIRECTS); + preserveMethodOnRedirect(Boolean.TRUE); + return self(); + } + + /** + * Get the preset {@link NettyConnectionController} or create an instance of the default one, if not preset. + * @return the {@link NettyConnectionController} instance. + */ + /* package */ NettyConnectionController connectionController() { + return connectionController.isPresent() ? connectionController.get() : new NettyConnectionController(); + } + + /** + * Update {@link #expect100ContinueTimeout(int) expect 100-Continue timeout} based on current http request properties. + * + * @param clientRequest the current http client request. + * @return updated configuration. + */ + /* package */ N expect100ContinueTimeout(ClientRequest clientRequest) { + expect100ContTimeout.set( + clientRequest.resolveProperty( + prefixed(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT), expect100ContTimeout.get())); + return this.self(); + } + + /** + * Return value of {@link #enableSslHostnameVerification(boolean)} setting either from the configuration of from the + * HTTP client request properties. The default is {@code true}. + * + * @param properties the HTTP client request properties. + * @return the value of SSL hostname verification setting. + */ + /* package */ boolean isSslHostnameVerificationEnabled(Map<String, Object> properties) { + return ClientProperties.getValue(properties, + prefixed(NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION), + enableHostnameVerification.get()); + } + + /** + * Update {@link #maxRedirects(int)} value from the HTTP Client request. + * @param request the HTTP Client request. + * @return maximum redirects value. + */ + /* package */ int maxRedirects(ClientRequest request) { + maxRedirects.set( + request.resolveProperty(prefixed(NettyClientProperties.MAX_REDIRECTS), maxRedirects.get())); + return maxRedirects.get(); + } + + /** + * Update the {@link #preserveMethodOnRedirect(boolean) preservation} of HTTP method during HTTP redirect + * by HTTP client request properties. + * + * @param request HTTP client request. + * @return the value of preservation. + */ + /* package */ boolean preserveMethodOnRedirect(ClientRequest request) { + preserveMethodOnRedirect.set( + request.resolveProperty( + prefixed(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT), preserveMethodOnRedirect.get())); + return preserveMethodOnRedirect.get(); + } + + /** + * Get the instance of preset {@link NettyHttpRedirectController} either from configuration, + * or from the HTTP client request, or the default if non set. + * @param request the HTTP client request. + * @return an instance of {@link NettyHttpRedirectController}. + */ + /* package */ NettyHttpRedirectController redirectController(ClientRequest request) { + NettyHttpRedirectController customRedirectController = + request.resolveProperty( + prefixed(NettyClientProperties.HTTP_REDIRECT_CONTROLLER), NettyHttpRedirectController.class); + if (customRedirectController == null) { + customRedirectController = redirectController.get(); + } + if (customRedirectController == null) { + customRedirectController = new NettyHttpRedirectController(); + } + + return customRedirectController; + } + + /** + * <p> + * Return a new instance of configuration updated by the merged settings from this and client properties. + * Only properties unresolved during the request are updated. + * </p><p> + * {@code This} is meant to be settings from the connector. + * The priorities should go DEFAULTS -> CONNECTOR -> CLIENT -> REQUEST. + * </p> + * + * @param client the REST client. + * @return a new instance of configuration. + */ + /* package */ N fromClient(Client client) { + final Map<String, Object> properties = client.getConfiguration().getProperties(); + final N clientConfiguration = copyFromClient(client.getConfiguration()); + + final Object threadPoolSize = properties.get(clientConfiguration.prefixed(ClientProperties.ASYNC_THREADPOOL_SIZE)); + if (threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) { + clientConfiguration.asyncThreadPoolSize((Integer) threadPoolSize); + } + + final Object maxPoolSizeTotalProperty = properties.get( + clientConfiguration.prefixed(NettyClientProperties.MAX_CONNECTIONS_TOTAL)); + final Object maxPoolIdleProperty = properties.get( + clientConfiguration.prefixed(NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT)); + final Object maxPoolSizeProperty = properties.get( + clientConfiguration.prefixed(NettyClientProperties.MAX_CONNECTIONS)); + + if (maxPoolSizeTotalProperty != null) { + clientConfiguration.maxPoolSizeTotal.set((Integer) maxPoolSizeTotalProperty); + } + + if (maxPoolIdleProperty != null) { + clientConfiguration.maxPoolIdle.set((Integer) maxPoolIdleProperty); + } + + if (maxPoolSizeProperty != null) { + clientConfiguration.maxPoolSize.set((Integer) maxPoolSizeProperty); + } + + if (clientConfiguration.maxPoolSizeTotal.get() < 0) { + throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_TOTAL(maxPoolSizeTotal.get())); + } + + if (clientConfiguration.maxPoolSize.get() < 0) { + throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_SIZE(maxPoolSize.get())); + } + + final Object logging = properties.get(clientConfiguration.prefixed(NettyClientProperties.LOGGING_ENABLED)); + if (logging instanceof Boolean) { + clientConfiguration.loggingEnabled.set((Boolean) logging); + } else if (logging instanceof String) { + clientConfiguration.loggingEnabled.set(Boolean.valueOf((String) logging)); + } + + return clientConfiguration; + } + + /** + * <p> + * Return a new instance of configuration updated by the merged settings from this and HTTP client request properties. + * Only properties unresolved during the request are updated. + * </p><p> + * {@code This} is meant to be settings from the connector. + * The priorities should go DEFAULTS -> CONNECTOR -> CLIENT -> REQUEST. + * </p> + + * @param request the HTTP client request. + * @return a new instance of configuration. + */ + /* package */ N fromRequest(ClientRequest request) { + final N requestConfiguration = copyFromRequest(request); + + final Boolean logging = request.resolveProperty(prefixed(NettyClientProperties.LOGGING_ENABLED), Boolean.class); + if (logging != null) { + requestConfiguration.loggingEnabled.set(logging); + } + + return requestConfiguration; + } + + + /** + * Create an instance of {@link HttpClientCodec} based on preset settings {@link #initialHttpHeaderLineLength(int)}, + * {@link #maxHeaderSize} and {@link #maxChunkSize}. The settings can be preset in the configuration or + * on the HTTP client request. + * + * @param properties The HTTP client request properties. + * @return the {@link HttpClientCodec} instance. + */ + /* package */ HttpClientCodec createHttpClientCodec(Map<String, Object> properties) { + firstHttpHeaderLineLength.set(ClientProperties.getValue(properties, + prefixed(NettyClientProperties.MAX_INITIAL_LINE_LENGTH), firstHttpHeaderLineLength.get())); + maxHeaderSize.set( + ClientProperties.getValue(properties, prefixed(NettyClientProperties.MAX_HEADER_SIZE), maxHeaderSize.get())); + maxChunkSize.set( + ClientProperties.getValue(properties, prefixed(NettyClientProperties.MAX_CHUNK_SIZE), maxChunkSize.get())); + + return new HttpClientCodec(firstHttpHeaderLineLength.get(), maxHeaderSize.get(), maxChunkSize.get()); + } + + /** + * Create an instance of {@link ProxyHandler} based on HTTP request URI's port and address, + * the preset proxy {@link #proxyUri(URI) uri}, {@link #proxyUserName(String) username}, + * and {@link #proxyPassword(String) password}. + * + * Can filter headers {@link #filterHeadersForProxy(boolean)}. + * + * @param clientProxy the Jersey {@link ClientProxy} instance. + * @param jerseyRequest the HTTP client request containing HTTP headers to be filtered. + * @return a Netty {@link ProxyHandler} instance. + */ + /* package */ ProxyHandler createProxyHandler(ClientProxy clientProxy, ClientRequest jerseyRequest) { + final URI u = clientProxy.uri(); + InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(), u.getPort() == -1 ? 8080 : u.getPort()); + + filterHeadersForProxy.set(jerseyRequest + .resolveProperty(prefixed(NettyClientProperties.FILTER_HEADERS_FOR_PROXY), filterHeadersForProxy.get())); + HttpHeaders httpHeaders = NettyConnector.setHeaders( + jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filterHeadersForProxy.get())); + + ProxyHandler proxy = clientProxy.userName() == null ? new HttpProxyHandler(proxyAddr, httpHeaders) + : new HttpProxyHandler(proxyAddr, clientProxy.userName(), clientProxy.password(), httpHeaders); + if (connectTimeout.get() > 0) { + proxy.setConnectTimeoutMillis(connectTimeout.get()); + } + + return proxy; + } + } + +}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java index cdf90dd..46f27a7 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025 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 @@ -51,8 +51,53 @@ @Beta public class NettyConnectorProvider implements ConnectorProvider { + private final Config config; + + public NettyConnectorProvider() { + this.config = config(); + } + + private NettyConnectorProvider(Config config) { + this.config = config; + } + @Override public Connector getConnector(Client client, Configuration runtimeConfig) { - return new NettyConnector(client); + return new NettyConnector(client, config.rw()); + } + + /** + * Instantiate a builder allowing to configure the NettyConnectorProvider. + * @return a new {@link Config} instance. + */ + public static Config config() { + return new Config(); + } + + public static final class Config extends NettyConnectorConfiguration<Config> { + + private Config() { + } + + /* package */ RW rw() { + RW rw = new RW(); + rw.setNonEmpty(this); + return rw; + } + + public NettyConnectorProvider build() { + return new NettyConnectorProvider(this); + } + + /* package */ static class RW extends ReadWrite<RW> { + @Override + public RW instance() { + return new RW(); + } + + public RW me() { + return this; + } + } } }
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java index 670be3e..6c4c41c 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java
@@ -34,9 +34,15 @@ */ public class NettyHttpRedirectController { + private NettyConnectorProvider.Config.RW configuration; + + /* package */ void init(NettyConnectorProvider.Config.RW configuration) { + this.configuration = configuration; + } + /** * Configure the HTTP request after HTTP Redirect response has been received. - * By default, the HTTP POST request is transformed into HTTP GET for status 301 & 302. + * By default, the HTTP POST request is not transformed into HTTP GET for status 301 & 302. * Also, HTTP Headers described by RFC 9110 Section 15.4 are removed from the new HTTP Request. * * @param request The new {@link ClientRequest} to be sent to the redirected URI. @@ -44,7 +50,7 @@ * @return {@code true} when the new request should be sent. */ public boolean prepareRedirect(ClientRequest request, ClientResponse response) { - final Boolean keepMethod = request.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE); + final boolean keepMethod = configuration.preserveMethodOnRedirect(request); if (Boolean.FALSE.equals(keepMethod) && request.getMethod().equals(HttpMethod.POST)) { switch (response.getStatus()) {
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java index bcd3fd8..f758f95 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025 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 @@ -16,9 +16,9 @@ package org.glassfish.jersey.netty.connector.internal; +import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.handler.stream.ChunkedInput; -import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.RequestEntityProcessing; @@ -28,6 +28,7 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; /** * The Entity Writer is used to write entity in Netty. One implementation is delayed, @@ -60,7 +61,7 @@ /** * Flushes the writen objects. Can throw IOException. - * @throws IOException + * @throws IOException exception. */ void flush() throws IOException; @@ -68,7 +69,7 @@ * Get the netty Chunked Input to be written. * @return The Chunked input instance */ - ChunkedInput getChunkedInput(); + ChunkedInput<ByteBuf> getChunkedInput(); /** * Get the {@link OutputStream} used to write an entity @@ -78,20 +79,20 @@ /** * Get the length of the entity written to the {@link OutputStream} - * @return + * @return length of the entity. */ long getLength(); /** - * Return Type of - * @return + * Return Type of the {@link NettyEntityWriter}. + * @return type of the writer. */ Type getType(); - static NettyEntityWriter getInstance(ClientRequest clientRequest, Channel channel) { + static NettyEntityWriter getInstance( + ClientRequest clientRequest, Channel channel, Supplier<RequestEntityProcessing> requestEntityProcessingSupplier) { final long lengthLong = clientRequest.getLengthLong(); - final RequestEntityProcessing entityProcessing = clientRequest.resolveProperty( - ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); + final RequestEntityProcessing entityProcessing = requestEntityProcessingSupplier.get(); if ((entityProcessing == null && lengthLong == -1) || entityProcessing == RequestEntityProcessing.CHUNKED) { return new DirectEntityWriter(channel, Type.CHUNKED); @@ -129,7 +130,7 @@ } @Override - public ChunkedInput getChunkedInput() { + public ChunkedInput<ByteBuf> getChunkedInput() { return stream; } @@ -203,7 +204,7 @@ } @Override - public ChunkedInput getChunkedInput() { + public ChunkedInput<ByteBuf> getChunkedInput() { return writer.getChunkedInput(); } @@ -226,7 +227,7 @@ private class DelayedOutputStream extends OutputStream { private final List<WriteAction> actions = new ArrayList<>(); private int writeLen = 0; - private AtomicBoolean streamFlushed = new AtomicBoolean(false); + private final AtomicBoolean streamFlushed = new AtomicBoolean(false); @Override public void write(int b) throws IOException {
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java index 1d0135d..22ce8d3 100644 --- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
@@ -83,10 +83,11 @@ ConnectorProvider provider = new ConnectorProvider() { @Override public Connector getConnector(Client client, Configuration runtimeConfig) { - return new NettyConnector(client) { + return new NettyConnector(client, NettyConnectorProvider.config().rw()) { @Override - NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) { - writer.set(super.nettyEntityWriter(clientRequest, channel)); + NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel, + NettyConnectorProvider.Config.RW config) { + writer.set(super.nettyEntityWriter(clientRequest, channel, config)); writerSetLatch.countDown(); return new NettyEntityWriter() { private boolean slept = false;
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java index 78a3448..fed5f61 100644 --- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025 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 @@ -223,9 +223,12 @@ return new ConnectorProvider() { @Override public Connector getConnector(Client client, Configuration runtimeConfig) { - return new NettyConnector(client) { - NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) { - NettyEntityWriter wrapped = NettyEntityWriter.getInstance(clientRequest, channel); + return new NettyConnector(client, NettyConnectorProvider.config().rw()) { + @Override + NettyEntityWriter nettyEntityWriter( + ClientRequest clientRequest, Channel channel, NettyConnectorProvider.Config.RW config) { + NettyEntityWriter wrapped = NettyEntityWriter.getInstance( + clientRequest, channel, () -> config.requestEntityProcessing(clientRequest)); JerseyChunkedInput chunkedInput = (JerseyChunkedInput) wrapped.getChunkedInput(); try {
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java new file mode 100644 index 0000000..6dd9b03 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java
@@ -0,0 +1,1683 @@ +/* + * Copyright (c) 2025 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.netty.connector; + +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpRequest; +import org.glassfish.jersey.client.ClientConfig; +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.innate.ClientProxy; +import org.glassfish.jersey.http.HttpHeaders; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.internal.util.collection.Ref; +import org.glassfish.jersey.client.innate.ConnectorConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Configuration; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class ConnectorConfigTest { + + private static final String PREFIX = "test."; + + private static ClientRequest createRequest(Client client) { + return new ClientRequest(URI.create("http://localhost:8080"), + (ClientConfig) client.getConfiguration(), new MapPropertiesDelegate()) { + }; + } + + @Test + public void testPrefixedConfig() { + ConnectorConfig.RW r = new ConnectorConfig.RW(); + Configuration prefixed, unprefixed; + + unprefixed = r.config(); + unprefixed.getProperties().put(ClientProperties.CONNECT_TIMEOUT, 1000); + prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed); + Assertions.assertNull(prefixed.getProperty(ClientProperties.CONNECT_TIMEOUT)); + Assertions.assertNull(prefixed.getProperties().get(ClientProperties.CONNECT_TIMEOUT)); + + unprefixed = r.config(); + unprefixed.getProperties().put(PREFIX + ClientProperties.CONNECT_TIMEOUT, 2000); + prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed); + Assertions.assertEquals(2000, prefixed.getProperty(ClientProperties.CONNECT_TIMEOUT)); + Assertions.assertEquals(2000, prefixed.getProperties().get(ClientProperties.CONNECT_TIMEOUT)); + + unprefixed = r.config(); + prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed); + unprefixed.getProperties().put(ClientProperties.CONNECT_TIMEOUT, 2000); + prefixed.getProperties().putAll(unprefixed.getProperties()); + Assertions.assertNull(prefixed.getProperty(ClientProperties.CONNECT_TIMEOUT)); + Assertions.assertNull(prefixed.getProperties().get(ClientProperties.CONNECT_TIMEOUT)); + + unprefixed = r.config(); + prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed); + unprefixed.getProperties().put(PREFIX + ClientProperties.CONNECT_TIMEOUT, 2000); + prefixed.getProperties().putAll(unprefixed.getProperties()); + Assertions.assertEquals(2000, prefixed.getProperty(ClientProperties.CONNECT_TIMEOUT)); + Assertions.assertEquals(2000, prefixed.getProperties().get(ClientProperties.CONNECT_TIMEOUT)); + + unprefixed = r.config(); + prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed); + prefixed.getProperties().put(PREFIX + ClientProperties.PROXY_USERNAME, "USERNAME"); + Assertions.assertEquals("USERNAME", prefixed.getProperty(ClientProperties.PROXY_USERNAME)); + prefixed.getProperties().put(ClientProperties.PROXY_PASSWORD, "PASSWORD"); + Assertions.assertNull(prefixed.getProperty(ClientProperties.PROXY_PASSWORD)); + } + + @Test + public void testAsyncThreadPoolSize() { + AtomicInteger result = new AtomicInteger(0); + class RWAsync extends RW { + @Override + public Integer asyncThreadPoolSize() { + result.set(super.asyncThreadPoolSize()); + return super.asyncThreadPoolSize(); + } + + @Override + public RWAsync instance() { + return new RWAsync(); + } + } + + Client client = ClientBuilder.newClient(); + NettyConnectorProvider.Config.RW rw0 = new RWAsync().asyncThreadPoolSize(10); + new NettyConnector(client, rw0); + Assertions.assertEquals(10, result.get()); + + result.set(0); + NettyConnectorProvider.Config.RW rw1 = new RWAsync().asyncThreadPoolSize(20); + client.property(ClientProperties.CONNECTOR_CONFIGURATION, rw1); + new NettyConnector(client, rw0); + Assertions.assertEquals(20, result.get()); + + result.set(0); + client.property(ClientProperties.ASYNC_THREADPOOL_SIZE, 30); + new NettyConnector(client, rw0); + Assertions.assertEquals(30, result.get()); + } + + @Test + public void testConnectTimeout() { + final AtomicInteger result = new AtomicInteger(0); + class RWConnect extends NettyConnectorProvider.Config.RW { + @Override + public int connectTimeout() { + result.set(super.connectTimeout()); + throw new IllegalStateException(); + } + + @Override + public RWConnect instance() { + return new RWConnect(); + } + } + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)).request().apply(); + Assertions.assertEquals(1000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECT_TIMEOUT, 3000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request().apply(); + Assertions.assertEquals(3000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECT_TIMEOUT, 3000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(4000))) + .apply(); + Assertions.assertEquals(3000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(4000))) + .apply(); + Assertions.assertEquals(4000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(4000))) + .request(r -> r.setProperty(ClientProperties.CONNECT_TIMEOUT, 5000)) + .apply(); + Assertions.assertEquals(5000, result.get()); + + result.set(0); + new TestClient(new RWConnect().prefix(PREFIX)).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + new RWConnect().connectTimeout(2000).prefix(PREFIX))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWConnect().connectTimeout(4000))) + .apply(); + Assertions.assertEquals(2000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWConnect().connectTimeout(4000).prefix(PREFIX))) + .request(r -> r.setProperty(ClientProperties.CONNECT_TIMEOUT, 5000)) + .apply(); + Assertions.assertEquals(4000, result.get()); + } + + @Test + public void testExpect100Continue() { + final AtomicReference<Boolean> result = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWExpect extends RW { + + @Override + public Boolean expect100Continue(ClientRequest request) { + result.set(super.expect100Continue(request)); + return result.get(); + } + + @Override + public int connectTimeout() { + config.set(this); + return super.connectTimeout(); + } + + @Override + public RWExpect instance() { + return new RWExpect(); + } + } + + Request req = new TestClient(new RWExpect()).request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertNull(result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100Continue(true)).request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100Continue(true).prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + ClientProperties.EXPECT_100_CONTINUE, false)) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100Continue(true).prefix(PREFIX)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100Continue(false))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWExpect()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100Continue(true))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100Continue(false))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().prefix(PREFIX)) + .client(c -> c.property(PREFIX + ClientProperties.EXPECT_100_CONTINUE, true)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWExpect().expect100Continue(false))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(true, result.get()); + } + + @Test + public void testExpect100ContinueThreshold() { + final AtomicReference<Long> result = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWExpect extends RW { + + @Override + public long expect100ContinueThreshold(ClientRequest request) { + result.set(super.expect100ContinueThreshold(request)); + return result.get(); + } + + @Override + public int connectTimeout() { + config.set(this); + return super.connectTimeout(); + } + + @Override + public RWExpect instance() { + return new RWExpect(); + } + } + + Request req = new TestClient(new RWExpect()).request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100ContinueThreshold(1000)).request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(1000, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100ContinueThreshold(1000).prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 2000)) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(2000, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100ContinueThreshold(1000).prefix(PREFIX)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100ContinueThreshold(2000))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(2000, result.get()); + + result.set(null); + req = new TestClient(new RWExpect()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + new RWExpect().expect100ContinueThreshold(1000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWExpect().expect100ContinueThreshold(2000))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(2000, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().prefix(PREFIX)) + .client(c -> c.property(PREFIX + ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 1000)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWExpect().expect100ContinueThreshold(2000))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(1000, result.get()); + } + + @Test + public void testExpect100ContinueTimeout() { + final AtomicReference<Integer> result = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWExpect extends RW { + + @Override + public NettyConnectorProvider.Config.RW expect100ContinueTimeout(ClientRequest request) { + super.expect100ContinueTimeout(request); + result.set(this.expect100ContTimeout.get()); + return this; + } + + @Override + public int connectTimeout() { + config.set(this); + return super.connectTimeout(); + } + + @Override + public RWExpect instance() { + return new RWExpect(); + } + } + + Request req = new TestClient(new RWExpect()).request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100ContinueTimeout(1000)).request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(1000, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100ContinueTimeout(1000).prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 2000)) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(2000, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().expect100ContinueTimeout(1000).prefix(PREFIX)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100ContinueTimeout(2000))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(2000, result.get()); + + result.set(null); + req = new TestClient(new RWExpect()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100ContinueTimeout(1000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWExpect().expect100ContinueTimeout(2000))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(2000, result.get()); + + result.set(null); + req = new TestClient(new RWExpect().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 1000)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWExpect().expect100ContinueTimeout(2000))) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(1000, result.get()); + } + + @Test + public void testFollowRedirects() { + final AtomicReference<Boolean> result = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWFollow extends RW { + + @Override + public boolean followRedirects() { + result.set(super.followRedirects()); + return result.get(); + } + + @Override + public int connectTimeout() { + config.set(this); + return super.connectTimeout(); + } + + @Override + public RWFollow instance() { + return new RWFollow(); + } + } + + Request req; + req = new TestClient(new RWFollow()).request().apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().followRedirects(); + Assertions.assertEquals(new RWFollow().copy().followRedirects(), result.get()); + + result.set(null); + req = new TestClient(new RWFollow().followRedirects(false)).request().apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().followRedirects(); + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWFollow().followRedirects(false).prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + ClientProperties.FOLLOW_REDIRECTS, true)) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().followRedirects(); + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWFollow().followRedirects(false).prefix(PREFIX)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWFollow().followRedirects(true))) + .apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().followRedirects(); + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWFollow()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWFollow().followRedirects(false))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWFollow().followRedirects(true))) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().followRedirects(); + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWFollow().prefix(PREFIX)) + .client(c -> c.property(PREFIX + ClientProperties.FOLLOW_REDIRECTS, false)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWFollow().followRedirects(true))) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().followRedirects(); + Assertions.assertEquals(false, result.get()); + } + + @Test + public void testProxy() { + final AtomicReference<ClientProxy> result = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWProxy extends RW { + + @Override + public Optional<ClientProxy> proxy(ClientRequest request, URI requestUri) { + Optional<ClientProxy> proxy = super.proxy(request, requestUri); + result.set(proxy.orElse(null)); + return proxy; + } + + @Override + public RWProxy instance() { + return new RWProxy(); + } + } + String proxyUri = "http://proxy.org:8080"; + String userName = "USERNAME"; + String password = "PASSWORD"; + + new TestClient(new RWProxy()).request().apply(); + Assertions.assertNull(result.get()); + + result.set(null); + new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)).request().apply(); + Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString()); + Assertions.assertEquals(userName, result.get().userName()); + Assertions.assertEquals(password, result.get().password()); + + result.set(null); + new TestClient(new RWProxy().prefix(PREFIX).proxyUri(proxyUri).proxyUserName("XXX").proxyPassword(password)) + .request(r -> r.setProperty(PREFIX + ClientProperties.PROXY_USERNAME, userName)) + .request(r -> r.setProperty(ClientProperties.PROXY_PASSWORD, "XXX")) + .apply(); + Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString()); + Assertions.assertEquals(userName, result.get().userName()); + Assertions.assertEquals(password, result.get().password()); + + result.set(null); + new TestClient(new RWProxy().prefix(PREFIX).proxyUri(proxyUri).proxyUserName("XXX").proxyPassword(password)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWProxy().proxyUserName(userName).proxyPassword(null))) + .apply(); + Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString()); + Assertions.assertEquals(userName, result.get().userName()); + Assertions.assertNull(result.get().password()); + + result.set(null); + new TestClient(new RWProxy().prefix(PREFIX) + .proxy(new java.net.Proxy(java.net.Proxy.Type.HTTP, + new InetSocketAddress(proxyUri.split("g:")[0] + "g", Integer.parseInt(proxyUri.split("g:")[1]))))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWProxy().proxyUserName(userName).proxyPassword(password).prefix(PREFIX))) + .apply(); + Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString()); + Assertions.assertEquals(userName, result.get().userName()); + Assertions.assertEquals(password, result.get().password()); + } + + @Test + public void testReadTimeout() { + final AtomicInteger result = new AtomicInteger(0); + class RWRead extends NettyConnectorProvider.Config.RW { + + @Override + public int readTimeout() { + result.set(super.readTimeout()); + throw new IllegalStateException(); + } + + @Override + public RWRead instance() { + return new RWRead(); + } + } + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)).request().apply(); + Assertions.assertEquals(1000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)) + .client(c -> c.property(ClientProperties.READ_TIMEOUT, 3000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000))) + .request().apply(); + Assertions.assertEquals(3000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)) + .client(c -> c.property(ClientProperties.READ_TIMEOUT, 3000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000))) + .apply(); + Assertions.assertEquals(3000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000))) + .apply(); + Assertions.assertEquals(4000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000))) + .request(r -> r.setProperty(ClientProperties.READ_TIMEOUT, 5000)) + .apply(); + Assertions.assertEquals(5000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + new RWRead().readTimeout(2000).prefix(PREFIX))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWRead().readTimeout(4000))) + .apply(); + Assertions.assertEquals(2000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWRead().readTimeout(4000).prefix(PREFIX))) + .request(r -> r.setProperty(ClientProperties.READ_TIMEOUT, 5000)) + .apply(); + Assertions.assertEquals(4000, result.get()); + } + + @Test + public void testRequestEntityProcessing() { + final AtomicReference<RequestEntityProcessing> result = new AtomicReference<>(); + final AtomicReference<RW> config = new AtomicReference<>(); + class RWRP extends RW { + + @Override + public RequestEntityProcessing requestEntityProcessing(ClientRequest request) { + result.set(super.requestEntityProcessing(request)); + return result.get(); + } + + @Override + public RWRP instance() { + config.set(new RWRP()); + return (RWRP) config.get(); + } + } + + Request req; + req = new TestClient(new RWRP().requestEntityProcessing(RequestEntityProcessing.CHUNKED)) + .request(r -> r.setEntity(PREFIX)).apply(); + try { + req.connector.nettyEntityWriter(req.request, null, config.get()); + } catch (NullPointerException ignore) { + } + Assertions.assertEquals(RequestEntityProcessing.CHUNKED, result.get()); + + result.set(null); + req = new TestClient(new RWRP().prefix(PREFIX).requestEntityProcessing(RequestEntityProcessing.CHUNKED)) + .request(r -> r.setEntity(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWRP().requestEntityProcessing(RequestEntityProcessing.BUFFERED).prefix(PREFIX))) + .apply(); + try { + req.connector.nettyEntityWriter(req.request, null, config.get()); + } catch (NullPointerException ignore) { + } + Assertions.assertEquals(RequestEntityProcessing.BUFFERED, result.get()); + + result.set(null); + req = new TestClient(new RWRP().prefix(PREFIX).requestEntityProcessing(RequestEntityProcessing.CHUNKED)) + .request(r -> r.setEntity(PREFIX)) + .request(r -> r.setProperty(PREFIX + ClientProperties.REQUEST_ENTITY_PROCESSING, + RequestEntityProcessing.BUFFERED)) + .apply(); + try { + req.connector.nettyEntityWriter(req.request, null, config.get()); + } catch (NullPointerException ignore) { + } + Assertions.assertEquals(RequestEntityProcessing.BUFFERED, result.get()); + } + + @Test + public void testSniHostName() { + final String sniHost = "sun.oracle.com"; + final AtomicReference<String> sniRef = new AtomicReference<>(); + final NettyConnectionController controller = new NettyConnectionController() { + @Override + public String getConnectionGroup(ClientRequest clientRequest, URI uri, String hostName, int port) { + sniRef.set(hostName); + return super.getConnectionGroup(clientRequest, uri, hostName, port); + } + }; + + final class RWSni extends RW { + @Override + public RWSni instance() { + return new RWSni(); + } + } + + new TestClient(new RWSni().connectionController(controller)) + .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, sniHost)) + .apply(); + Assertions.assertEquals(sniHost, sniRef.get()); + + new TestClient(new RWSni().connectionController(controller).sniHostName(sniHost)) + .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, "moon.oracle.com")) + .apply(); + Assertions.assertEquals(sniHost, sniRef.get()); + + new TestClient(new RWSni().connectionController(controller).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().sniHostName("moon.oracle.com"))) + .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, sniHost)) + .apply(); + Assertions.assertEquals(sniHost, sniRef.get()); + + new TestClient(new RWSni().connectionController(controller).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().sniHostName(sniHost).prefix(PREFIX))) + .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, "moon.oracle.com")) + .apply(); + Assertions.assertEquals(sniHost, sniRef.get()); + + new TestClient(new RWSni().connectionController(controller).prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().sniHostName("moon.oracle.com"))) + .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, sniHost)) + .apply(); + Assertions.assertEquals(sniHost, sniRef.get()); + + new TestClient(new RWSni().connectionController(controller).prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().sniHostName(sniHost).prefix(PREFIX))) + .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, "moon.oracle.com")) + .apply(); + Assertions.assertEquals(sniHost, sniRef.get()); + } + + @Test + public void testSslContext() { + final SSLContext testContext = new SSLContext(null, null, null){}; + final AtomicReference<SSLContext> result = new AtomicReference<>(); + final AtomicReference<RW> config = new AtomicReference<>(); + class RWSsl extends RW { + + @Override + public SSLContext sslContext(Client client, ClientRequest request) { + result.set(super.sslContext(client, request)); + return result.get(); + } + + @Override + public RWSsl instance() { + RWSsl rw = new RWSsl(); + config.set(rw); + return rw; + } + } + + Request req; + req = new TestClient(new RWSsl().sslContextSupplier(() -> testContext)).request().apply(); + config.get().sslContext(req.client, req.request); + Assertions.assertEquals(testContext, result.get()); + + result.set(null); + req = new TestClient(new RWSsl().prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + ClientProperties.SSL_CONTEXT_SUPPLIER, + (Supplier<SSLContext>) () -> testContext)).apply(); + config.get().sslContext(req.client, req.request); + Assertions.assertEquals(testContext, result.get()); + + result.set(null); + req = new TestClient(new RWSsl().prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWSsl().prefix(PREFIX).sslContextSupplier(() -> testContext))).apply(); + config.get().sslContext(req.client, req.request); + Assertions.assertEquals(testContext, result.get()); + } + + @Test + public void testConnectionController() { + final NettyConnectionController connectionController = new NettyConnectionController(); + final AtomicReference<NettyConnectionController> result = new AtomicReference<>(); + + class RWController extends RW { + + @Override + NettyConnectionController connectionController() { + result.set(super.connectionController()); + return result.get(); + } + + @Override + public RWController instance() { + return new RWController(); + } + } + + new TestClient(new RWController().connectionController(connectionController)).request().apply(); + Assertions.assertEquals(connectionController, result.get()); + + result.set(null); + new TestClient(new RWController().prefix(PREFIX).connectionController(new NettyConnectionController())) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWController().connectionController(connectionController).prefix(PREFIX))).apply(); + Assertions.assertEquals(connectionController, result.get()); + } + + @Test + public void testEnableSslHostnameVerification() { + final AtomicReference<Boolean> result = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWEnabled extends RW { + + @Override + boolean isSslHostnameVerificationEnabled(Map<String, Object> properties) { + result.set(super.isSslHostnameVerificationEnabled(properties)); + return result.get(); + } + + @Override + public RWEnabled instance() { + RWEnabled enabled = new RWEnabled(); + config.set(enabled); + return enabled; + } + } + + Request req; + req = new TestClient(new RWEnabled()).request().apply(); + config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(new RWEnabled().copy().followRedirects(), result.get()); + + result.set(null); + req = new TestClient(new RWEnabled().enableSslHostnameVerification(false)).request().apply(); + config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(false, result.get()); + +// NOT PER REQUEST +// result.set(null); +// req = new TestClient(new RWEnabled().enableSslHostnameVerification(false).prefix(PREFIX)) +// .request(r -> r.setProperty(PREFIX + NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true)) +// .apply(); +// config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties()); +// Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWEnabled().enableSslHostnameVerification(false).prefix(PREFIX)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWEnabled().enableSslHostnameVerification(true))) + .apply(); + config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWEnabled()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + new RWEnabled().enableSslHostnameVerification(false))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWEnabled().enableSslHostnameVerification(true))) + .apply(); + config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWEnabled().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, false)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWEnabled().enableSslHostnameVerification(true))) + .request(r -> r.setEntity(PREFIX)).apply(); + config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(false, result.get()); + } + + @Test + public void testMaxRedirects() { + final AtomicReference<Integer> result = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWMaxRed extends RW { + + @Override + int maxRedirects(ClientRequest request) { + result.set(super.maxRedirects(request)); + return result.get(); + } + + @Override + public RWMaxRed instance() { + RWMaxRed max = new RWMaxRed(); + config.set(max); + return max; + } + } + + Request req = new TestClient(new RWMaxRed()).request().apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + Assertions.assertEquals(new RWMaxRed().copy().maxRedirects(req.request), result.get()); + + result.set(null); + req = new TestClient(new RWMaxRed().maxRedirects(2)).request().apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + Assertions.assertEquals(2, result.get()); + + result.set(null); + req = new TestClient(new RWMaxRed().maxRedirects(2).prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + NettyClientProperties.MAX_REDIRECTS, 3)) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + Assertions.assertEquals(3, result.get()); + + result.set(null); + req = new TestClient(new RWMaxRed().maxRedirects(2).prefix(PREFIX)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWMaxRed().maxRedirects(3))) + .apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + Assertions.assertEquals(3, result.get()); + + result.set(null); + req = new TestClient(new RWMaxRed()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWMaxRed().maxRedirects(2))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWMaxRed().maxRedirects(3))) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + Assertions.assertEquals(3, result.get()); + + result.set(null); + req = new TestClient(new RWMaxRed().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_REDIRECTS, 2)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWMaxRed().maxRedirects(3))) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + Assertions.assertEquals(2, result.get()); + } + + @Test + public void testRedirectController() { + final NettyHttpRedirectController controller = new NettyHttpRedirectController(); + final AtomicReference<NettyHttpRedirectController> result = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWController extends RW { + + @Override + NettyHttpRedirectController redirectController(ClientRequest request) { + result.set(super.redirectController(request)); + return result.get(); + } + + @Override + public RWController instance() { + RWController max = new RWController(); + config.set(max); + return max; + } + } + + Request req; + + result.set(null); + req = new TestClient(new RWController().redirectController(controller)).request().apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().redirectController(req.request); + Assertions.assertEquals(controller, result.get()); + + result.set(null); + req = new TestClient(new RWController().redirectController(new NettyHttpRedirectController()).prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + NettyClientProperties.HTTP_REDIRECT_CONTROLLER, controller)) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().redirectController(req.request); + Assertions.assertEquals(controller, result.get()); + + result.set(null); + req = new TestClient(new RWController().redirectController(new NettyHttpRedirectController()).prefix(PREFIX)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWController().redirectController(controller))) + .apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + config.get().redirectController(req.request); + Assertions.assertEquals(controller, result.get()); + + result.set(null); + req = new TestClient(new RWController()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + new RWController().redirectController(new NettyHttpRedirectController()))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWController().redirectController(controller))) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); +// config.get().redirectController(req.request); + Assertions.assertEquals(controller, result.get()); + + result.set(null); + req = new TestClient(new RWController().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.HTTP_REDIRECT_CONTROLLER, controller)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWController().redirectController(new NettyHttpRedirectController()))) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); +// config.get().redirectController(req.request); + Assertions.assertEquals(controller, result.get()); + } + + @Test + public void testPreserveMethodOnRedirect() { + final AtomicReference<Boolean> result = new AtomicReference<>(); + final AtomicReference<NettyHttpRedirectController> controller = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWController extends RW { + + @Override + boolean preserveMethodOnRedirect(ClientRequest request) { + result.set(super.preserveMethodOnRedirect(request)); + return result.get(); + } + + @Override + NettyHttpRedirectController redirectController(ClientRequest request) { + controller.set(super.redirectController(request)); + return controller.get(); + } + + @Override + public RWController instance() { + RWController max = new RWController(); + config.set(max); + return max; + } + } + + Request req; + req = new TestClient(new RWController()).request().apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + try { + controller.get().prepareRedirect(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(new RWController().copy().preserveMethodOnRedirect(req.request), result.get()); + + result.set(null); + req = new TestClient(new RWController().preserveMethodOnRedirect(false)).request().apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + try { + controller.get().prepareRedirect(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWController().preserveMethodOnRedirect(true).prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + try { + controller.get().prepareRedirect(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWController().preserveMethodOnRedirect(true).prefix(PREFIX)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWController().preserveMethodOnRedirect(false))) + .apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + try { + controller.get().prepareRedirect(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWController()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + new RWController().preserveMethodOnRedirect(true))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWController().preserveMethodOnRedirect(false))) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + try { + controller.get().prepareRedirect(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWController().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWController().preserveMethodOnRedirect(true))) + .request(r -> r.setEntity(PREFIX)).apply(); + new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(), + new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get()); + try { + controller.get().prepareRedirect(req.request, null); + } catch (NullPointerException expected) { + } + Assertions.assertEquals(false, result.get()); + } + + @Test + public void testFilterHeadersForProxy() { + final AtomicReference<ClientProxy> proxy = new AtomicReference<>(); + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWProxy extends RW { + @Override + public Optional<ClientProxy> proxy(ClientRequest request, URI requestUri) { + Optional<ClientProxy> oProxy = super.proxy(request, requestUri); + proxy.set(oProxy.orElse(null)); + return oProxy; + } + + @Override + public RWProxy instance() { + RWProxy rw = new RWProxy(); + config.set(rw); + return rw; + } + } + String proxyUri = "http://proxy.org:8080"; + String userName = "USERNAME"; + String password = "PASSWORD"; + + Request req; + req = new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)).request().apply(); + config.get().createProxyHandler(proxy.get(), req.request); + Assertions.assertEquals(((Ref<Boolean>) new RWProxy().copy().filterHeadersForProxy).get(), + ((Ref<Boolean>) config.get().filterHeadersForProxy).get()); + + config.set(null); + req = new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password) + .filterHeadersForProxy(false)).request().apply(); + config.get().createProxyHandler(proxy.get(), req.request); + Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get()); + + config.set(null); + req = new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password) + .prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + NettyClientProperties.FILTER_HEADERS_FOR_PROXY, false)) + .apply(); + config.get().createProxyHandler(proxy.get(), req.request); + Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get()); + + config.set(null); + new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password) + .prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.FILTER_HEADERS_FOR_PROXY, false)) + .request().apply(); + config.get().createProxyHandler(proxy.get(), req.request); + Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get()); + + config.set(null); + new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password) + .prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().filterHeadersForProxy(false).prefix(PREFIX))) + .request().apply(); + config.get().createProxyHandler(proxy.get(), req.request); + Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get()); + + config.set(null); + new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password) + .prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().filterHeadersForProxy(false).prefix(PREFIX))) + .apply(); + config.get().createProxyHandler(proxy.get(), req.request); + Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get()); + } + + @Test + public void testFirstHttpHeaderLineLength() { + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + final AtomicReference<Integer> result = new AtomicReference<>(); + class RWFHHLL extends RW { + + @Override + HttpClientCodec createHttpClientCodec(Map<String, Object> properties) { + HttpClientCodec codec = super.createHttpClientCodec(properties); + result.set(firstHttpHeaderLineLength.get()); + return codec; + } + + @Override + public RWFHHLL instance() { + RWFHHLL rw = new RWFHHLL(); + config.set(rw); + return rw; + } + } + + Request req; + req = new TestClient(new RWFHHLL()).request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH, result.get()); + + result.set(null); + req = new TestClient(new RWFHHLL().initialHttpHeaderLineLength(5555)).request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWFHHLL().initialHttpHeaderLineLength(1111).prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWFHHLL().initialHttpHeaderLineLength(5555).prefix(PREFIX))).apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWFHHLL().initialHttpHeaderLineLength(1111).prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_INITIAL_LINE_LENGTH, 5555)) + .request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWFHHLL().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_INITIAL_LINE_LENGTH, 5555)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWFHHLL().initialHttpHeaderLineLength(8888).prefix(PREFIX))) + .apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + } + + @Test + public void testLoggingHandler() { + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + class RWLog extends RW { + @Override + public RWLog instance() { + RWLog rw = new RWLog(); + config.set(rw); + return rw; + } + } + + new TestClient(new RWLog()).request().apply(); + Assertions.assertFalse(config.get().loggingEnabled.get()); + + new TestClient(new RWLog().enableLoggingHandler(true)).request().apply(); + Assertions.assertTrue(config.get().loggingEnabled.get()); + + new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX)) + .request(r -> r.setProperty(NettyClientProperties.LOGGING_ENABLED, true)) + .apply(); + Assertions.assertFalse(config.get().loggingEnabled.get()); + + new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.LOGGING_ENABLED, true)) + .request() + .apply(); + Assertions.assertTrue(config.get().loggingEnabled.get()); + + new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + NettyClientProperties.LOGGING_ENABLED, true)) + .apply(); + Assertions.assertTrue(config.get().loggingEnabled.get()); + + new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX)) + .client(c -> c.property(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWLog().enableLoggingHandler(true))) + .request() + .apply(); + Assertions.assertTrue(config.get().loggingEnabled.get()); + + new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWLog().enableLoggingHandler(true).prefix(PREFIX))) + .apply(); + Assertions.assertTrue(config.get().loggingEnabled.get()); + } + + @Test + public void testMaxHeaderSize() { + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + final AtomicReference<Integer> result = new AtomicReference<>(); + class RWSize extends RW { + + @Override + HttpClientCodec createHttpClientCodec(Map<String, Object> properties) { + HttpClientCodec codec = super.createHttpClientCodec(properties); + result.set(maxHeaderSize.get()); + return codec; + } + + @Override + public RWSize instance() { + RWSize rw = new RWSize(); + config.set(rw); + return rw; + } + } + + Request req; + req = new TestClient(new RWSize()).request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(NettyClientProperties.DEFAULT_HEADER_SIZE, result.get()); + + result.set(null); + req = new TestClient(new RWSize().maxHeaderSize(5555)).request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWSize().maxHeaderSize(1111).prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWSize().maxHeaderSize(5555).prefix(PREFIX))).apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWSize().maxHeaderSize(1111).prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_HEADER_SIZE, 5555)) + .request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWSize().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_HEADER_SIZE, 5555)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWSize().maxHeaderSize(8888).prefix(PREFIX))) + .apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + } + + @Test + public void testMaxChunkSize() { + final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>(); + final AtomicReference<Integer> result = new AtomicReference<>(); + class RWSize extends RW { + + @Override + HttpClientCodec createHttpClientCodec(Map<String, Object> properties) { + HttpClientCodec codec = super.createHttpClientCodec(properties); + result.set(maxChunkSize.get()); + return codec; + } + + @Override + public RWSize instance() { + RWSize rw = new RWSize(); + config.set(rw); + return rw; + } + } + + Request req; + req = new TestClient(new RWSize()).request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(NettyClientProperties.DEFAULT_CHUNK_SIZE, result.get()); + + result.set(null); + req = new TestClient(new RWSize().maxChunkSize(5555)).request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWSize().maxChunkSize(1111).prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWSize().maxChunkSize(5555).prefix(PREFIX))).apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWSize().maxChunkSize(1111).prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_CHUNK_SIZE, 5555)) + .request().apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + + result.set(null); + req = new TestClient(new RWSize().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_CHUNK_SIZE, 5555)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWSize().maxChunkSize(8888).prefix(PREFIX))) + .apply(); + config.get().createHttpClientCodec(req.request.getConfiguration().getProperties()); + Assertions.assertEquals(5555, result.get()); + } + + @Test + public void testMaxTotalConnections() { + final AtomicReference<Integer> result = new AtomicReference<>(); + class RWConn extends RW { + + @Override + NettyConnectorProvider.Config.RW fromClient(Client client) { + NettyConnectorProvider.Config.RW rw = super.fromClient(client); + result.set(rw.maxPoolSizeTotal.get()); + return rw; + } + + @Override + public RWConn instance() { + return new RWConn(); + } + } + + new TestClient(new RWConn().maxTotalConnections(55)).request().apply(); + Assertions.assertEquals(55, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_CONNECTIONS_TOTAL, 55)) + .request().apply(); + Assertions.assertEquals(55, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().prefix(PREFIX).maxTotalConnections(55))) + .request().apply(); + Assertions.assertEquals(55, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + // NOT PER REQUEST + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().prefix(PREFIX).maxTotalConnections(55))) + .apply(); + Assertions.assertEquals(new RWConn().copy().maxPoolSizeTotal.get(), result.get()); + } + + @Test + public void testMaxConnectionsPerDestination() { + final AtomicReference<Integer> result = new AtomicReference<>(); + class RWConn extends RW { + + @Override + NettyConnectorProvider.Config.RW fromClient(Client client) { + NettyConnectorProvider.Config.RW rw = super.fromClient(client); + result.set(rw.maxPoolSize.get()); + return rw; + } + + @Override + public RWConn instance() { + return new RWConn(); + } + } + + new TestClient(new RWConn().maxConnectionsPerDestination(15)).request().apply(); + Assertions.assertEquals(15, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.MAX_CONNECTIONS, 15)) + .request().apply(); + Assertions.assertEquals(15, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().prefix(PREFIX).maxConnectionsPerDestination(15))) + .request().apply(); + Assertions.assertEquals(15, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + // NOT PER REQUEST + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().prefix(PREFIX).maxConnectionsPerDestination(15))) + .apply(); + Assertions.assertEquals(new RWConn().copy().maxPoolSize.get(), result.get()); + } + + @Test + public void testIdleConnectionPruneTimeout() { + final AtomicReference<Integer> result = new AtomicReference<>(); + class RWConn extends RW { + + @Override + NettyConnectorProvider.Config.RW fromClient(Client client) { + NettyConnectorProvider.Config.RW rw = super.fromClient(client); + result.set(rw.maxPoolIdle.get()); + return rw; + } + + @Override + public RWConn instance() { + return new RWConn(); + } + } + + new TestClient(new RWConn().idleConnectionPruneTimeout(15)).request().apply(); + Assertions.assertEquals(15, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + .client(c -> c.property(PREFIX + NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT, 15)) + .request().apply(); + Assertions.assertEquals(15, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().prefix(PREFIX).idleConnectionPruneTimeout(15))) + .request().apply(); + Assertions.assertEquals(15, result.get()); + + result.set(null); + new TestClient(new RWConn().prefix(PREFIX)) + // NOT PER REQUEST + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + NettyConnectorProvider.config().prefix(PREFIX).idleConnectionPruneTimeout(15))) + .apply(); + Assertions.assertEquals(new RWConn().copy().maxPoolIdle.get(), result.get()); + } + + @Test + public void testPrecedence() { + NettyConnectorProvider.Config.RW builderLower = NettyConnectorProvider.config().rw(); + builderLower.maxTotalConnections(55); + + NettyConnectorProvider.Config.RW builderUpper = builderLower.copy(); + builderUpper.maxTotalConnections(56); + Assertions.assertEquals(56, builderUpper.maxPoolSizeTotal.get()); + + Client client = ClientBuilder.newClient(); + client.property(NettyClientProperties.MAX_CONNECTIONS_TOTAL, 57); + NettyConnectorProvider.Config.RW result = builderUpper.fromClient(client); + Assertions.assertEquals(57, result.maxPoolSizeTotal.get()); + Assertions.assertEquals(60, result.maxPoolIdle.get()); + } + + private static class ConnectorConfig extends ConnectorConfiguration<ConnectorConfig> { + private static class RW extends ConnectorConfiguration<RW> implements Read<RW> { + @Override + public RW instance() { + return new RW(); + } + + @Override + public RW me() { + return this; + } + + public Configuration config() { + Map<String, Object> empty = new HashMap<>(); + Configuration configuration = (Configuration) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{Configuration.class}, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "getProperties": + return empty; + case "getProperty": + return empty.get(args[0]); + } + return null; + } + }); + + return configuration; + } + } + } + + private static class RW extends NettyConnectorProvider.Config.RW { + @Override + public int connectTimeout() { + throw new IllegalStateException(); + } + } + + + private static class TestClient { + private final NettyConnectorProvider.Config.RW rw; + private final Client client; + + private TestClient(NettyConnectorProvider.Config.RW rw) { + this.rw = rw; + this.client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new NettyConnectorProvider())); + } + + public TestClient client(Consumer<Client> consumer) { + consumer.accept(client); + return this; + } + + public TestClient rw(Consumer<NettyConnectorProvider.Config.RW> consumer) { + consumer.accept(rw); + return this; + } + + public Request request() { + return new Request(client, rw); + } + + public Request request(Consumer<ClientRequest> consumer) { + return request().request(consumer); + } + } + + private static class Request { + final ClientRequest request; + final Client client; + final NettyConnectorProvider.Config.RW rw; + NettyConnector connector; + + Request(Client client, NettyConnectorProvider.Config.RW rw) { + this.client = client; + this.rw = rw; + request = createRequest(client); + } + + public Request request(Consumer<ClientRequest> consumer) { + consumer.accept(request); + return this; + } + + public Request apply() { + try { + connector = new NettyConnector(client, rw); + connector.apply(request); + } catch (ProcessingException expected) { + } + return this; + } + } +}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java new file mode 100644 index 0000000..abf23e5 --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java
@@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 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.netty.connector; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.util.concurrent.atomic.AtomicBoolean; + +public class CustomConnectionControllerTest extends JerseyTest { + final AtomicBoolean hit = new AtomicBoolean(false); + + @Path("/") + public static class CustomConnectionControllerTestResource { + @GET + public String get() { + return "ok"; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(CustomConnectionControllerTestResource.class); + } + + @Override + protected void configureClient(ClientConfig config) { + NettyConnectorProvider provider = NettyConnectorProvider.config().connectionController(new NettyConnectionController() { + @Override + public String getConnectionGroup(ClientRequest clientRequest, URI uri, String hostName, int port) { + hit.set(true); + return super.getConnectionGroup(clientRequest, uri, hostName, port); + } + }).build(); + + config.connectorProvider(provider); + } + + @Test + public void testCustomConnectionControllerIsInvoked() { + try (Response response = target().request().get()) { + Assertions.assertEquals(200, response.getStatus()); + } + client().close(); + Assertions.assertEquals(true, hit.get()); + + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java index 267664d..559b7ad 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025 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 @@ -155,6 +155,15 @@ public static final String BACKGROUND_SCHEDULER_THREADPOOL_SIZE = "jersey.config.client.backgroundScheduler.threadPoolSize"; /** + * The connector configuration object available through connector provider configuration methods. + * + * <p> + * The name of the configuration property is <tt>{@value}</tt>. + * </p> + */ + public static final String CONNECTOR_CONFIGURATION = "jersey.config.client.ConnectorConfiguration"; + + /** * If {@link org.glassfish.jersey.client.filter.EncodingFilter} is * registered, this property indicates the value of Content-Encoding * property the filter should be adding.
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java index dd96928..b8ec60b 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025 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 @@ -20,14 +20,12 @@ import java.net.HttpURLConnection; import java.net.Proxy; import java.net.URL; -import java.util.Map; -import java.util.logging.Logger; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.core.Configuration; import org.glassfish.jersey.client.internal.HttpUrlConnector; -import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.client.internal.HttpUrlConnectorConfiguration; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.client.spi.ConnectorProvider; @@ -110,26 +108,22 @@ */ public static final String SET_METHOD_WORKAROUND = "jersey.config.client.httpUrlConnection.setMethodWorkaround"; - /** - * Default connection factory to be used. - */ - private static final ConnectionFactory DEFAULT_CONNECTION_FACTORY = new DefaultConnectionFactory(); - private static final Logger LOGGER = Logger.getLogger(HttpUrlConnectorProvider.class.getName()); - - private ConnectionFactory connectionFactory; - private int chunkSize; - private boolean useFixedLengthStreaming; - private boolean useSetMethodWorkaround; + protected final Config config; /** * Create new {@link java.net.HttpURLConnection}-based Jersey client connector provider. */ public HttpUrlConnectorProvider() { - this.connectionFactory = DEFAULT_CONNECTION_FACTORY; - this.chunkSize = ClientProperties.DEFAULT_CHUNK_SIZE; - this.useFixedLengthStreaming = false; - this.useSetMethodWorkaround = false; + this.config = new Config(); + } + + private HttpUrlConnectorProvider(Config config) { + this.config = config; + } + + public static Config config() { + return new Config(); } /** @@ -140,11 +134,7 @@ * @throws java.lang.NullPointerException in case the supplied connectionFactory is {@code null}. */ public HttpUrlConnectorProvider connectionFactory(final ConnectionFactory connectionFactory) { - if (connectionFactory == null) { - throw new NullPointerException(LocalizationMessages.NULL_INPUT_PARAMETER("connectionFactory")); - } - - this.connectionFactory = connectionFactory; + config.connectionFactory(connectionFactory); return this; } @@ -164,10 +154,7 @@ * @throws java.lang.IllegalArgumentException in case the specified chunk size is negative. */ public HttpUrlConnectorProvider chunkSize(final int chunkSize) { - if (chunkSize < 0) { - throw new IllegalArgumentException(LocalizationMessages.NEGATIVE_INPUT_PARAMETER("chunkSize")); - } - this.chunkSize = chunkSize; + config.chunkSize(chunkSize); return this; } @@ -183,7 +170,7 @@ * @return updated connector provider instance. */ public HttpUrlConnectorProvider useFixedLengthStreaming() { - this.useFixedLengthStreaming = true; + config.useFixedLengthStreaming(true); return this; } @@ -199,28 +186,18 @@ * @return updated connector provider instance. */ public HttpUrlConnectorProvider useSetMethodWorkaround() { - this.useSetMethodWorkaround = true; + config.useSetMethodWorkaround(true); return this; } @Override - public Connector getConnector(final Client client, final Configuration config) { - final Map<String, Object> properties = config.getProperties(); - - int computedChunkSize = ClientProperties.getValue(properties, - ClientProperties.CHUNKED_ENCODING_SIZE, chunkSize, Integer.class); - if (computedChunkSize < 0) { - LOGGER.warning(LocalizationMessages.NEGATIVE_CHUNK_SIZE(computedChunkSize, chunkSize)); - computedChunkSize = chunkSize; - } - - final boolean computedUseFixedLengthStreaming = ClientProperties.getValue(properties, - USE_FIXED_LENGTH_STREAMING, useFixedLengthStreaming, Boolean.class); - final boolean computedUseSetMethodWorkaround = ClientProperties.getValue(properties, - SET_METHOD_WORKAROUND, useSetMethodWorkaround, Boolean.class); - - return createHttpUrlConnector(client, connectionFactory, computedChunkSize, computedUseFixedLengthStreaming, - computedUseSetMethodWorkaround); + public Connector getConnector(final Client client, final Configuration configuration) { + this.config.preInit(configuration); + return createHttpUrlConnector(client, + this.config.connectionFactory(), + this.config.chunkSize(), + this.config.fixLengthStreaming(), + this.config.setMethodWorkaround()); } /** @@ -230,7 +207,7 @@ * @param connectionFactory {@link javax.net.ssl.HttpsURLConnection} factory to be used when creating * connections. * @param chunkSize chunk size to use when using HTTP chunked transfer coding. - * @param fixLengthStreaming specify if the the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int) + * @param fixLengthStreaming specify if the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int) * fixed-length streaming mode} on the underlying HTTP URL connection instances should * be used when sending requests. * @param setMethodWorkaround specify if the reflection workaround should be used to set HTTP URL connection method @@ -240,12 +217,33 @@ protected Connector createHttpUrlConnector(Client client, ConnectionFactory connectionFactory, int chunkSize, boolean fixLengthStreaming, boolean setMethodWorkaround) { - return new HttpUrlConnector( - client, - connectionFactory, - chunkSize, - fixLengthStreaming, - setMethodWorkaround); + return new HttpUrlConnector(client, client.getConfiguration(), config); + } + + public static final class Config extends HttpUrlConnectorConfiguration<Config> { + public HttpUrlConnectorProvider build() { + return new HttpUrlConnectorProvider(this); + } + + /* package */ ConnectionFactory connectionFactory() { + return connectionFactory.get(); + } + + /* package */ int chunkSize() { + return chunkSize.get(); + } + + /* package */ boolean fixLengthStreaming() { + return useFixedLengthStreaming.get(); + } + + /* package */ boolean setMethodWorkaround() { + return useSetMethodWorkaround.get(); + } + + /* package */ void preInit(Configuration configuration) { + super.preInit(configuration.getProperties()); + } } /** @@ -291,23 +289,6 @@ } } - private static class DefaultConnectionFactory implements ConnectionFactory { - - @Override - public HttpURLConnection getConnection(final URL url) throws IOException { - return connect(url, null); - } - - @Override - public HttpURLConnection getConnection(URL url, Proxy proxy) throws IOException { - return connect(url, proxy); - } - - private HttpURLConnection connect(URL url, Proxy proxy) throws IOException { - return (proxy == null) ? (HttpURLConnection) url.openConnection() : (HttpURLConnection) url.openConnection(proxy); - } - } - @Override public boolean equals(final Object o) { if (this == o) { @@ -318,22 +299,11 @@ } final HttpUrlConnectorProvider that = (HttpUrlConnectorProvider) o; - - if (chunkSize != that.chunkSize) { - return false; - } - if (useFixedLengthStreaming != that.useFixedLengthStreaming) { - return false; - } - - return connectionFactory.equals(that.connectionFactory); + return config.equals(that.config); } @Override public int hashCode() { - int result = connectionFactory.hashCode(); - result = 31 * result + chunkSize; - result = 31 * result + (useFixedLengthStreaming ? 1 : 0); - return result; + return config.hashCode(); } }
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java index 68ab324..1d82b06 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2025 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 @@ -16,8 +16,8 @@ package org.glassfish.jersey.client.innate; import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.internal.LocalizationMessages; +import org.glassfish.jersey.internal.PropertiesResolver; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.core.Configuration; @@ -31,6 +31,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Locale; +import java.util.Map; import java.util.Optional; /** @@ -42,16 +43,20 @@ // do not instantiate }; - public static Optional<ClientProxy> proxyFromRequest(ClientRequest request) { + public static Optional<ClientProxy> proxyFromRequest(PropertiesResolver request) { return getProxy(request); } - public static Optional<ClientProxy> proxyFromProperties(URI requestUri) { + public static Optional<ClientProxy> proxyFromUri(URI requestUri) { return getSystemPropertiesProxy(requestUri); } + public static Optional<ClientProxy> proxyFromProperties(Map<String, Object> properties) { + return getProxy(properties); + } + public static Optional<ClientProxy> proxyFromConfiguration(Configuration configuration) { - return getProxy(configuration); + return getProxy(configuration.getProperties()); } public static ClientProxy proxy(Proxy proxy) { @@ -103,7 +108,7 @@ return userName; }; - private static Optional<ClientProxy> getProxy(ClientRequest request) { + private static Optional<ClientProxy> getProxy(PropertiesResolver request) { Object proxyUri = request.resolveProperty(ClientProperties.PROXY_URI, Object.class); if (proxyUri != null) { ClientProxy proxy = toProxy(proxyUri); @@ -118,13 +123,13 @@ return Optional.empty(); } - private static Optional<ClientProxy> getProxy(Configuration config) { - Object proxyUri = config.getProperties().get(ClientProperties.PROXY_URI); + private static Optional<ClientProxy> getProxy(Map<String, Object> properties) { + Object proxyUri = properties.get(ClientProperties.PROXY_URI); if (proxyUri != null) { ClientProxy proxy = toProxy(proxyUri); if (proxy != null) { - proxy.userName = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_USERNAME, String.class); - proxy.password = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class); + proxy.userName = ClientProperties.getValue(properties, ClientProperties.PROXY_USERNAME, String.class); + proxy.password = ClientProperties.getValue(properties, ClientProperties.PROXY_PASSWORD, String.class); return Optional.of(proxy); } else { return Optional.empty();
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/ConnectorConfiguration.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/ConnectorConfiguration.java new file mode 100644 index 0000000..48e9244 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/ConnectorConfiguration.java
@@ -0,0 +1,895 @@ +/* + * Copyright (c) 2025 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.client.innate; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.client.innate.http.SSLParamConfigurator; +import org.glassfish.jersey.internal.PropertiesResolver; + +import javax.net.ssl.SSLContext; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Feature; +import java.net.Proxy; +import java.net.URI; +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * Configuration object to use for configuring the client connectors and HTTP request processing. + * This configuration provides settings to be handled by the connectors, mainly declared by {@link ClientProperties}. + * + * @param <E> the connector configuration subtype. + */ +public class ConnectorConfiguration<E extends ConnectorConfiguration<E>> { + protected final NullableRef<Integer> connectTimeout = NullableRef.empty(); + protected final NullableRef<Boolean> expect100Continue = NullableRef.empty(); + protected final NullableRef<Long> expect100continueThreshold = NullableRef.empty(); + protected final NullableRef<Boolean> followRedirects = NullableRef.empty(); + protected final NullableRef<String> prefix = NullableRef.empty(); + protected final NullableRef<Object> proxyUri = NullableRef.empty(); + protected final NullableRef<String> proxyUserName = NullableRef.empty(); + protected final NullableRef<String> proxyPassword = NullableRef.empty(); + protected final NullableRef<Integer> readTimeout = NullableRef.empty(); + protected final NullableRef<RequestEntityProcessing> requestEntityProcessing = NullableRef.empty(); + protected final NullableRef<String> sniHostname = NullableRef.empty(); + protected final NullableRef<Supplier<SSLContext>> sslContextSupplier = NullableRef.empty(); + protected final NullableRef<Integer> threadPoolSize = NullableRef.empty(); + + /** + * Use factory methods provided by each connector supporting this configuration object and its subclass instead. + */ + protected ConnectorConfiguration() { + } + + /** + * Set the asynchronous thread-pool size. The property {@link ClientProperties#ASYNC_THREADPOOL_SIZE} + * has precedence over this setting. + * + * @param threadPoolSize the size of the asynchronous thread-pool. + * @return updated configuration. + */ + public E asyncThreadPoolSize(int threadPoolSize) { + this.threadPoolSize.set(threadPoolSize); + return self(); + } + + /** + * Set connect timeout. The property {@link ClientProperties#CONNECT_TIMEOUT} + * has precedence over this setting. + * + * @param millis timeout in milliseconds. + * @return updated configuration. + */ + public E connectTimeout(int millis) { + connectTimeout.set(millis); + return self(); + } + + /** + * Allows for HTTP Expect:100-Continue. + * The property {@link ClientProperties#EXPECT_100_CONTINUE} has precedence over this setting. + * + * @param enable allows for HTTP Expect:100-Continue or not. + * @return updated configuration. + */ + public E expect100Continue(boolean enable) { + expect100Continue.set(enable); + return self(); + } + + /** + * Set the Expect:100-Continue content-length threshold size. + * The {@link ClientProperties#EXPECT_100_CONTINUE_THRESHOLD_SIZE} property has precedence over this setting. + * + * @param size the content-length threshold. + * @return updated configuration. + */ + public E expect100ContinueThreshold(long size) { + expect100continueThreshold.set(size); + return self(); + } + + /** + * Set to follow redirects. The property {@link ClientProperties#FOLLOW_REDIRECTS} has precedence over this setting. + * + * @param follow to follow or not to follow. + * @return updated configuration. + */ + public E followRedirects(boolean follow) { + followRedirects.set(follow); + return self(); + } + + /** + * <p> + * Set the prefix for the configuration properties used by Client/Request to configure and override the settings. + * For instance, if the prefix would be {@code com.example.MyProject.}, the property {@link #connectTimeout(int)} + * is overridden only by properties with key starting by the prefix, + * i.e. for {@link ClientProperties#CONNECT_TIMEOUT}, + * the property key {@code com.example.MyProject.jersey.config.client.connectTimeout} would override the setting. + * </p> + * <p> + * The prefix can be used to override the settings by the System property set specifically for the + * prefixed connector. See {@link org.glassfish.jersey.CommonProperties#ALLOW_SYSTEM_PROPERTIES_PROVIDER} + * for enabling System properties usage. + * </p> + * <p> + * The default configuration prefix is empty. + * </p> + * + * @param prefix the non-null prefix. + * @throws NullPointerException if the prefix is null. + * @return updated configuration. + */ + public E prefix(String prefix) { + this.prefix.set(Objects.requireNonNull(prefix)); + return self(); + } + + /** + * Set proxy password. The property {@link ClientProperties#PROXY_PASSWORD} + * has precedence over this setting. + * + * @param proxyPassword the proxy password. + * @return updated configuration. + */ + public E proxyPassword(String proxyPassword) { + this.proxyPassword.set(proxyPassword); + return self(); + } + + /** + * Set proxy username. The property {@link ClientProperties#PROXY_USERNAME} + * has precedence over this setting. + * + * @param userName the proxy username. + * @return updated configuration. + */ + public E proxyUserName(String userName) { + proxyUserName.set(userName); + return self(); + } + + /** + * Set proxy URI. The property {@link ClientProperties#PROXY_URI} + * has precedence over this setting. + * + * @param proxyUri the proxy URI. + * @return updated configuration. + */ + public E proxyUri(String proxyUri) { + this.proxyUri.set(proxyUri); + return self(); + } + + /** + * Set proxy URI. The property {@link ClientProperties#PROXY_URI} + * has precedence over this setting. + * + * @param proxyUri the proxy URI. + * @return updated configuration. + */ + public E proxyUri(URI proxyUri) { + this.proxyUri.set(proxyUri); + return self(); + } + + /** + * Set HTTP proxy. The property {@link ClientProperties#PROXY_URI} + * has precedence over this setting. + * + * @param proxy the HTTP proxy. + * @return updated configuration. + */ + public E proxy(Proxy proxy) { + this.proxyUri.set(proxy); + return self(); + } + + /** + * Set read timeout. The property {@link ClientProperties#READ_TIMEOUT} + * has precedence over this setting. + * + * @param millis timeout in milliseconds. + * @return updated configuration. + */ + public E readTimeout(int millis) { + readTimeout.set(millis); + return self(); + } + + /** + * Set the request entity processing type. + * + * @param requestEntityProcessing the request entity processing type. + * @return the updated configuration. + */ + public E requestEntityProcessing(RequestEntityProcessing requestEntityProcessing) { + this.requestEntityProcessing.set(requestEntityProcessing); + return self(); + } + + public E sniHostName(String sniHostname) { + this.sniHostname.set(sniHostname); + return self(); + } + + /** + * Set the {@link SSLContext} supplier. The property {@link ClientProperties#SSL_CONTEXT_SUPPLIER} has precedence over + * this setting. + * + * @param sslContextSupplier the {@link SSLContext} supplier. + * @return the updated configuration. + */ + public E sslContextSupplier(Supplier<SSLContext> sslContextSupplier) { + this.sslContextSupplier.set(sslContextSupplier); + return self(); + } + + /** + * Return type-cast self. + * @return self. + */ + @SuppressWarnings("unchecked") + protected E self() { + return (E) this; + } + + /** + * <p> + * A reference to a value. The reference can be empty, but unlike the {@code Optional}, once a value is set, + * it never can be empty again. The {@code null} value is treated as a non-empty value of null. + * </p><p> + * This {@code null} + * can be used to override some previous configuration value, to distinguish the intentional {@code null} override + * from an empty (non-set) configuration value. + * </p> + * @param <T> type of the value. + */ + protected static class NullableRef<T> implements org.glassfish.jersey.internal.util.collection.Ref<T> { + + private NullableRef() { + // use factory methods; + } + + /** + * Return a new empty reference. + * + * @return an empty reference. + * @param <T> The type of the empty value. + */ + public static <T> NullableRef<T> empty() { + return new NullableRef<>(); + } + + /** + * Return a reference of a given value. + * + * @param value the value this reference refers to.* + * @return a new reference to a given value. + * @param <T> type of the value. + */ + public static <T> NullableRef<T> of(T value) { + NullableRef<T> ref = new NullableRef<>(); + ref.set(value); + return ref; + } + + private boolean empty = true; + private T ref = null; + + @Override + public void set(T value) { + empty = false; + ref = value; + } + + /** + * Set or replace the value if other value is set. + * @param other a reference to another value. + */ + public void setNonEmpty(NullableRef<T> other) { + other.ifPresent(this::set); + } + + @Override + public T get() { + return ref; + } + + /** + * Run action if and only if the condition applies. + * + * @param predicate the condition to be met. + * @param action the action to run if condition is met. + */ + public void iff(Predicate<T> predicate, Runnable action) { + if (predicate.test(ref)) { + action.run(); + } + } + + /** + * If it is empty, sets the {@code value} value. Keeps the original value, otherwise. + * + * @param value the value to be set if empty. + */ + public void ifEmptySet(T value) { + if (empty) { + set(value); + } + } + + /** + * If a value is present, performs the given action with the value, + * otherwise does nothing. + * + * @param action the action to be performed, if a value is present + * @throws NullPointerException if value is present and the given action is + * {@code null} + */ + public void ifPresent(Consumer<? super T> action) { + if (!empty) { + action.accept(ref); + } + } + + /** + * If a value is present, performs the given action with the value, + * otherwise performs the given empty-based action. + * + * @param action the action to be performed, if a value is present + * @param emptyAction the empty-based action to be performed, if no value is + * present + * @throws NullPointerException if a value is present and the given action + * is {@code null}, or no value is present and the given empty-based + * action is {@code null}. + */ + public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) { + if (!empty) { + action.accept(ref); + } else { + emptyAction.run(); + } + } + + /** + * Return the value if present, the {@code other} otherwise. + * + * @param other the value if not present + * @return inner value if present or the other otherwise. + */ + public T ifPresentOrElse(T other) { + return empty ? other : ref; + } + + /** + * If a value is not present, returns {@code true}, otherwise + * {@code false}. + * + * @return {@code true} if a value is not present, otherwise {@code false} + */ + public boolean isEmpty() { + return empty; + } + + /** + * If a value is present, returns {@code true}, otherwise {@code false}. + * + * @return {@code true} if a value is present, otherwise {@code false} + */ + public boolean isPresent() { + return !empty; + } + + + @Override + public int hashCode() { + return Objects.hash(ref, empty); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof NullableRef)) { + return false; + } + + NullableRef<?> that = (NullableRef<?>) o; + return Objects.equals(empty, that.empty) && Objects.equals(ref, that.ref); + } + + @Override + public String toString() { + return empty ? "<empty>" : ref == null ? "<null>" : ref.toString(); + } + } + + protected interface Read<CC extends ConnectorConfiguration<CC> & Read<CC>> + extends SSLParamConfigurator.SSLParamConfiguratorConfiguration { + + /** + * Set and replace the values of current configuration by values of other configuration + * if and only if the values of other configuration are set. + * + * @param other another configuration instance. + */ + public default <X extends ConnectorConfiguration<?>> void setNonEmpty(X other) { + me().connectTimeout.setNonEmpty(other.connectTimeout); + me().expect100Continue.setNonEmpty(other.expect100Continue); + me().expect100continueThreshold.setNonEmpty(other.expect100continueThreshold); + me().followRedirects.setNonEmpty(other.followRedirects); + me().prefix.setNonEmpty(other.prefix); + me().proxyUri.setNonEmpty(other.proxyUri); + me().proxyUserName.setNonEmpty(other.proxyUserName); + me().proxyPassword.setNonEmpty(other.proxyPassword); + me().readTimeout.setNonEmpty(other.readTimeout); + me().requestEntityProcessing.setNonEmpty(other.requestEntityProcessing); + me().sniHostname.setNonEmpty(other.sniHostname); + me().sslContextSupplier.setNonEmpty(other.sslContextSupplier); + me().threadPoolSize.setNonEmpty(other.threadPoolSize); + } + + /** + * Return the thread-pool size setting. + * + * @return the thread pool size setting. + */ + public default Integer asyncThreadPoolSize() { + return me().threadPoolSize.get(); + } + + /** + * Update connect timeout value based on request properties settings. + * + * @param request the current HTTP client request. + * @return the updated configuration. + */ + public default int connectTimeout(ClientRequest request) { + me().connectTimeout.set( + request.resolveProperty(prefixed(ClientProperties.CONNECT_TIMEOUT), me().connectTimeout.get()) + ); + return me().connectTimeout.get(); + } + + /** + * Get the value of connect timeout setting. + * + * @return connect timeout value. + */ + public default int connectTimeout() { + return me().connectTimeout.get(); + } + + /** + * Sets the default value. The default methods cannot be set on instances passed by the customers using + * {@link ClientProperties#CONNECTOR_CONFIGURATION} since they would override the values previously set + * by the connector configuration object. + * @return the initialized configuration object. + */ + public default CC init() { + me().connectTimeout(0) + .expect100ContinueThreshold(ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE) + .followRedirects(Boolean.TRUE) + .prefix("") + .readTimeout(0); + return me(); + } + + /** + * Utility method to create a new instance of configuration to preserve the settings of previous configuration. + * + * @return a new instance of the configuration. + */ + public default CC copy() { + CC config = instance(); + config.init(); + config.setNonEmpty(me()); + return config; + } + + public default CC copyFromClient(Configuration configuration) { + CC clientConfiguration = copy(); + final Map<String, Object> properties = configuration.getProperties(); + Object configProp = properties.get(clientConfiguration.prefixed(ClientProperties.CONNECTOR_CONFIGURATION)); + if (configProp != null) { + ConnectorConfiguration<?> clientCfg = (ConnectorConfiguration<?>) configProp; + if (me().prefix.equals(clientCfg.prefix) || clientCfg.prefix.get() == null) { + clientConfiguration.setNonEmpty(clientCfg); + } + } else { + configProp = properties.get(ClientProperties.CONNECTOR_CONFIGURATION); + if (configProp != null && me().prefix.equals(((ConnectorConfiguration<?>) configProp).prefix)) { + clientConfiguration.setNonEmpty((ConnectorConfiguration<?>) configProp); + } + } + return clientConfiguration; + } + + public default CC copyFromRequest(ClientRequest request) { + CC requestConfiguration = copy(); + Object configProp = request.getProperty(prefixed(ClientProperties.CONNECTOR_CONFIGURATION)); + if (configProp != null) { + ConnectorConfiguration<?> requestCfg = (ConnectorConfiguration<?>) configProp; + if (me().prefix.equals(requestCfg.prefix) || requestCfg.prefix.get() == null) { + requestConfiguration.setNonEmpty(requestCfg); + } + } else { + configProp = request.getProperty(ClientProperties.CONNECTOR_CONFIGURATION); + if (configProp != null && me().prefix.equals(((ConnectorConfiguration<?>) configProp).prefix)) { + requestConfiguration.setNonEmpty((ConnectorConfiguration<?>) configProp); + } + } + return requestConfiguration; + } + + @Override + default String getSniHostNameProperty(Configuration configuration) { + Object property = configuration.getProperty(prefixed(ClientProperties.SNI_HOST_NAME)); + if (property == null) { + property = configuration.getProperty(prefixed(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT))); + } + return property == null ? me().sniHostname.get() : (String) property; + } + + /** + * Update the {@link #expect100Continue(boolean)} from the HTTP client request. + * + * @param request the HTTP client request. + * @return the Expect: 100-Continue support value. + */ + public default Boolean expect100Continue(ClientRequest request) { + final Boolean expectContinueActivated = + request.resolveProperty(prefixed(ClientProperties.EXPECT_100_CONTINUE), Boolean.class); + if (expectContinueActivated != null) { + me().expect100Continue.set(expectContinueActivated); + } + return me().expect100Continue.get(); + } + + /** + * Update the {@link #expect100ContinueThreshold(long)} from the HTTP client request. + * + * @param request the HTTP client request. + * @return the content length threshold size. + */ + public default long expect100ContinueThreshold(ClientRequest request) { + me().expect100continueThreshold.set( + request.resolveProperty(prefixed(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE), + me().expect100continueThreshold.get()) + ); + return me().expect100continueThreshold.get(); + } + + /** + * Update the {@link #followRedirects(boolean)} setting from the HTTP client request. The default is {@code true}. + * + * @param request the HTTP client request. + * @return follow redirects setting. + */ + public default boolean followRedirects(ClientRequest request) { + me().followRedirects.set( + request.resolveProperty(prefixed(ClientProperties.FOLLOW_REDIRECTS), me().followRedirects.get()) + ); + return me().followRedirects.get(); + } + + /** + * Get the value of the follow redirects setting. The default is {@code true}. + * + * @return whether to follow redirects or not. + */ + public default boolean followRedirects() { + return me().followRedirects.get(); + } + + public default Configuration prefixedConfiguration(Configuration configuration) { + return me().prefix.get().isEmpty() ? configuration : new PrefixedConfiguration(me().prefix.get(), configuration); + } + + /** + * Create optional client proxy information based on the proxy information set in the configuration + * or the HTTP client request. The used settings are {@link #proxy(Proxy)}, + * {@link #proxyUri(URI)}, {@link #proxyUri(String)}, {@link #proxyUserName(String)}, + * and {@link #proxyPassword(String)}. + * + * @param request the HTTP client request, + * @param requestUri the HTTP request URI. It can differ from the URI used in the request, based on other + * information set by the HTTP client request. + * @return the optional client proxy. + */ + public default Optional<ClientProxy> proxy(ClientRequest request, URI requestUri) { + Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest( + me().prefix.get().isEmpty() + ? request + : new PrefixedPropertiesResolver(me().prefix.get(), request) + ); + if (!proxy.isPresent() && me().proxyUri.isPresent()) { + final Map<String, Object> properties = me().prefix.get().isEmpty() + ? new HashMap<>() + : new PrefixedMap<>(me().prefix.get(), new HashMap<>()); + properties.put(me().prefix.get() + ClientProperties.PROXY_URI, me().proxyUri.get()); + properties.put(me().prefix.get() + ClientProperties.PROXY_USERNAME, me().proxyUserName.get()); + properties.put(me().prefix.get() + ClientProperties.PROXY_PASSWORD, me().proxyPassword.get()); + request.getPropertyNames().forEach(k -> properties.put(k, request.getProperty(k))); + proxy = ClientProxy.proxyFromProperties(properties); + } + if (!proxy.isPresent()) { + proxy = ClientProxy.proxyFromUri(requestUri); + } + return proxy; + } + + /** + * Update {@link #readTimeout(int) read timeout} based on the HTTP request properties. + * + * @param request the current HTTP client request. + * @return updated configuration. + */ + public default CC readTimeout(ClientRequest request) { + me().readTimeout.set(request.resolveProperty(prefixed(ClientProperties.READ_TIMEOUT), me().readTimeout.get())); + return me(); + } + + /** + * Get the value of preset {@link #readTimeout(int)}. + * + * @return the read timeout milliseconds. + */ + public default int readTimeout() { + return me().readTimeout.get(); + } + + /** + * Get the {@link RequestEntityProcessing} updated by the HTTP client request. + * + * @param request the HTTP client request. + * @return the RequestEntityProcessing type. + */ + public default RequestEntityProcessing requestEntityProcessing(ClientRequest request) { + RequestEntityProcessing entityProcessing = + request.resolveProperty(prefixed(ClientProperties.REQUEST_ENTITY_PROCESSING), RequestEntityProcessing.class); + if (entityProcessing == null) { + entityProcessing = me().requestEntityProcessing.get(); + } + return entityProcessing; + } + + @Override + default String resolveSniHostNameProperty(PropertiesResolver resolver) { + String property = resolver.resolveProperty(prefixed(ClientProperties.SNI_HOST_NAME), String.class); + if (property == null) { + property = resolver.resolveProperty( + prefixed(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT)), String.class); + } + return property == null ? me().sniHostname.get() : property; + } + + /** + * Get {@link SSLContext} either from the {@link ClientProperties#SSL_CONTEXT_SUPPLIER}, or from this configuration, + * or from the {@link Client#getSslContext()} in this order. + * + * @param client the client used to get the {@link SSLContext}. + * @param request the request used to get the {@link SSLContext}. + * @return the {@link SSLContext}. + */ + public default SSLContext sslContext(Client client, ClientRequest request) { + @SuppressWarnings("unchecked") + Supplier<SSLContext> supplier = + request.resolveProperty(prefixed(ClientProperties.SSL_CONTEXT_SUPPLIER), Supplier.class); + if (supplier == null) { + supplier = me().sslContextSupplier.get(); + } + return supplier == null ? client.getSslContext() : supplier.get(); + } + + public default String prefixed(String propertyName) { + return me().prefix.get() + propertyName; + } + + /** + * Return a new instance of configuration. + * @return a new instance of configuration. + */ + public CC instance(); + + /** + * Return typed-cast self. + * @return self. + */ + public CC me(); + } + + + /** + * A properties map that works with prefixed properties. + * + * @param <V> Object type. + */ + private static class PrefixedMap<V> implements Map<String, V> { + private final Map<String, V> inner; + private final String prefix; + + private PrefixedMap(String prefix, Map<String, V> inner) { + this.inner = inner; + this.prefix = prefix; + } + + @Override + public int size() { + return inner.size(); + } + + @Override + public boolean isEmpty() { + return inner.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return inner.containsKey(prefix + key); + } + + @Override + public boolean containsValue(Object value) { + return inner.containsValue(value); + } + + @Override + public V get(Object key) { + return inner.get(prefix + key); + } + + @Override + public V put(String key, V value) { + return inner.put(key, value); + } + + @Override + public V remove(Object key) { + return inner.remove(prefix + key); + } + + @Override + public void putAll(Map<? extends String, ? extends V> m) { + inner.putAll(m); + } + + @Override + public void clear() { + inner.clear(); + } + + @Override + public Set<String> keySet() { + return inner.keySet(); + } + + @Override + public Collection<V> values() { + return inner.values(); + } + + @Override + public Set<Entry<String, V>> entrySet() { + return inner.entrySet(); + } + } + + /** + * Properties resolver that resolves prefixed properties. + */ + private static class PrefixedPropertiesResolver implements PropertiesResolver { + private final String prefix; + private final PropertiesResolver resolver; + + private PrefixedPropertiesResolver(String prefix, PropertiesResolver resolver) { + this.prefix = prefix; + this.resolver = resolver; + } + + @Override + public <T> T resolveProperty(String name, Class<T> type) { + return resolver.resolveProperty(prefix + name, type); + } + + @Override + public <T> T resolveProperty(String name, T defaultValue) { + return resolver.resolveProperty(prefix + name, defaultValue); + } + } + + protected static class PrefixedConfiguration implements Configuration { + private final String prefix; + private final Configuration inner; + + private PrefixedConfiguration(String prefix, Configuration inner) { + this.prefix = prefix; + this.inner = inner; + } + + @Override + public RuntimeType getRuntimeType() { + return inner.getRuntimeType(); + } + + @Override + public Map<String, Object> getProperties() { + return new PrefixedMap<>(prefix, inner.getProperties()); + } + + @Override + public Object getProperty(String name) { + return inner.getProperty(prefix + name); + } + + @Override + public Collection<String> getPropertyNames() { + return inner.getPropertyNames(); + } + + @Override + public boolean isEnabled(Feature feature) { + return inner.isEnabled(feature); + } + + @Override + public boolean isEnabled(Class<? extends Feature> featureClass) { + return inner.isEnabled(featureClass); + } + + @Override + public boolean isRegistered(Object component) { + return inner.isRegistered(component); + } + + @Override + public boolean isRegistered(Class<?> componentClass) { + return inner.isRegistered(componentClass); + } + + @Override + public Map<Class<?>, Integer> getContracts(Class<?> componentClass) { + return inner.getContracts(componentClass); + } + + @Override + public Set<Class<?>> getClasses() { + return inner.getClasses(); + } + + @Override + public Set<Object> getInstances() { + return inner.getInstances(); + } + } +}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java index 212e83d..93e1cb7 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
@@ -20,7 +20,6 @@ import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.http.HttpHeaders; import org.glassfish.jersey.internal.PropertiesResolver; -import org.glassfish.jersey.internal.guava.InetAddresses; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; @@ -52,6 +51,11 @@ private String sniHostNameHeader = null; private String sniHostPrecedence = null; private boolean setAlways = false; + private final SSLParamConfiguratorConfiguration configConfiguration; + + public Builder(SSLParamConfiguratorConfiguration configuration) { + this.configConfiguration = configuration; + } /** @@ -61,7 +65,7 @@ */ public Builder request(ClientRequest clientRequest) { this.sniHostNameHeader = getSniHostNameHeader(clientRequest.getHeaders()); - this.sniHostPrecedence = resolveSniHostNameProperty(clientRequest); + this.sniHostPrecedence = configConfiguration.resolveSniHostNameProperty(clientRequest); this.uri = clientRequest.getUri(); return this; } @@ -72,7 +76,7 @@ * @return the builder instance */ public Builder configuration(Configuration configuration) { - this.sniHostPrecedence = getSniHostNameProperty(configuration); + this.sniHostPrecedence = this.configConfiguration.getSniHostNameProperty(configuration); return this; } @@ -141,7 +145,7 @@ * @return the builder instance. */ public Builder setSNIHostName(Configuration configuration) { - return setSNIHostName(getSniHostNameProperty(configuration)); + return setSNIHostName(this.configConfiguration.getSniHostNameProperty(configuration)); } /** @@ -160,7 +164,7 @@ * @return the builder instance. */ public Builder setSNIHostName(PropertiesResolver resolver) { - return setSNIHostName(resolveSniHostNameProperty(resolver)); + return setSNIHostName(configConfiguration.resolveSniHostNameProperty(resolver)); } /** @@ -180,22 +184,6 @@ final String hostHeader = hostHeaders.get(0).toString(); return hostHeader; } - - private static String resolveSniHostNameProperty(PropertiesResolver resolver) { - String property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME, String.class); - if (property == null) { - property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT), String.class); - } - return property; - } - - private static String getSniHostNameProperty(Configuration configuration) { - Object property = configuration.getProperty(ClientProperties.SNI_HOST_NAME); - if (property == null) { - property = configuration.getProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT)); - } - return (String) property; - } } private SSLParamConfigurator(SSLParamConfigurator.Builder builder) { @@ -212,7 +200,14 @@ * Create a new instance of TlsSupport class **/ public static SSLParamConfigurator.Builder builder() { - return new SSLParamConfigurator.Builder(); + return new SSLParamConfigurator.Builder(DEFAULT_CONFIGURATION); + } + + /** + * Create a new instance of TlsSupport class + **/ + public static SSLParamConfigurator.Builder builder(SSLParamConfiguratorConfiguration configuration) { + return new SSLParamConfigurator.Builder(configuration); } /** @@ -290,4 +285,25 @@ sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); sslEngine.setSSLParameters(sslParameters); } + + public static interface SSLParamConfiguratorConfiguration { + public default String getSniHostNameProperty(Configuration configuration) { + Object property = configuration.getProperty(ClientProperties.SNI_HOST_NAME); + if (property == null) { + property = configuration.getProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT)); + } + return (String) property; + } + + public default String resolveSniHostNameProperty(PropertiesResolver resolver) { + String property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME, String.class); + if (property == null) { + property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT), String.class); + } + return property; + } + } + + private static final SSLParamConfiguratorConfiguration DEFAULT_CONFIGURATION = new SSLParamConfiguratorConfiguration() { + }; }
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java index 630cb96..e1fd816 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025 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 @@ -52,6 +52,7 @@ import javax.net.ssl.SSLSocketFactory; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.core.Configuration; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; @@ -81,7 +82,6 @@ public class HttpUrlConnector implements Connector { private static final Logger LOGGER = Logger.getLogger(HttpUrlConnector.class.getName()); - private static final String ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY = "sun.net.http.allowRestrictedHeaders"; // Avoid multi-thread uses of HttpsURLConnection.getDefaultSSLSocketFactory() because it does not implement a // proper lazy-initialization. See https://github.com/jersey/jersey/issues/3293 private static final LazyValue<SSLSocketFactory> DEFAULT_SSL_SOCKET_FACTORY = @@ -110,11 +110,7 @@ } } - private final HttpUrlConnectorProvider.ConnectionFactory connectionFactory; - private final int chunkSize; - private final boolean fixLengthStreaming; - private final boolean setMethodWorkaround; - private final boolean isRestrictedHeaderPropertySet; + private final HttpUrlConnectorConfiguration.ReadWrite clientConfig; private Value<SSLSocketFactory> sslSocketFactory; // SSLContext#getSocketFactory not idempotent @@ -131,7 +127,7 @@ * @param client JAX-RS client instance for which the connector is being created. * @param connectionFactory {@link javax.net.ssl.HttpsURLConnection} factory to be used when creating connections. * @param chunkSize chunk size to use when using HTTP chunked transfer coding. - * @param fixLengthStreaming specify if the the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int) + * @param fixLengthStreaming specify if the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int) * fixed-length streaming mode} on the underlying HTTP URL connection instances should be * used when sending requests. * @param setMethodWorkaround specify if the reflection workaround should be used to set HTTP URL connection method @@ -144,29 +140,23 @@ final boolean fixLengthStreaming, final boolean setMethodWorkaround) { - this.connectionFactory = connectionFactory; - this.chunkSize = chunkSize; - this.fixLengthStreaming = fixLengthStreaming; - this.setMethodWorkaround = setMethodWorkaround; + this(client, client.getConfiguration(), + HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory) + .chunkSize(chunkSize) + .useFixedLengthStreaming(fixLengthStreaming) + .useSetMethodWorkaround(setMethodWorkaround) + ); + } + public HttpUrlConnector(Client client, Configuration configuration, HttpUrlConnectorConfiguration<?> config) { + this.clientConfig = config.rw().fromClient(configuration); this.sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() { @Override public SSLSocketFactory get() { return client.getSslContext().getSocketFactory(); } }); - - // check if sun.net.http.allowRestrictedHeaders system property has been set and log the result - // the property is being cached in the HttpURLConnection, so this is only informative - there might - // already be some connection(s), that existed before the property was set/changed. - isRestrictedHeaderPropertySet = Boolean.valueOf(AccessController.doPrivileged( - PropertiesHelper.getSystemProperty(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY, "false") - )); - - LOGGER.config(isRestrictedHeaderPropertySet - ? LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_TRUE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY) - : LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_FALSE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY) - ); } private static InputStream getInputStream(final HttpURLConnection uc, final ClientRequest clientRequest) throws IOException { @@ -342,9 +332,15 @@ * @param clientRequest the actual client request. * @param uc http connection to be secured. */ - private void secureConnection( - final ClientRequest clientRequest, final HttpURLConnection uc, final SSLParamConfigurator sniConfig) { - setSslContextFactory(clientRequest.getClient(), clientRequest); + private void secureConnection(final ClientRequest clientRequest, + final HttpURLConnection uc, + final SSLParamConfigurator sniConfig, + final HttpUrlConnectorConfiguration.ReadWrite config) { + if (config.isSslContextSupplier() || config.isPrefixed()) { + setSslContextFactory(clientRequest.getClient(), clientRequest, config.sslContext(clientRequest)); + } else { + setSslContextFactory(clientRequest.getClient(), clientRequest); + } secureConnection(clientRequest.getClient(), uc); // keep this for compatibility if (sniConfig.isSNIRequired() && uc instanceof HttpsURLConnection) { // set SNI @@ -355,9 +351,17 @@ } } + @Deprecated protected void setSslContextFactory(Client client, ClientRequest request) { - final Supplier<SSLContext> supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class); + setSslContextFactory(client, request, request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class)); + } + protected void setSslContextFactory(Client client, ClientRequest request, + HttpUrlConnectorConfiguration.ReadWrite requestConfig) { + setSslContextFactory(client, request, requestConfig.sslContext(request)); + } + + private void setSslContextFactory(Client client, ClientRequest request, Supplier<SSLContext> supplier) { if (supplier != null) { sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() { // lazy for double-check locking if multiple requests @Override @@ -375,9 +379,10 @@ } private ClientResponse _apply(final ClientRequest request) throws IOException { + HttpUrlConnectorConfiguration.ReadWrite requestConfiguration = clientConfig.fromRequest(request); + final HttpURLConnection uc; - final Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest(request); - final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().request(request) + final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder(requestConfiguration).request(request) .setSNIHostName(request).build(); final URI sniUri; if (sniConfig.isSNIRequired()) { @@ -391,39 +396,45 @@ DEFAULT_SSL_SOCKET_FACTORY.get(); } + final Optional<ClientProxy> proxy = requestConfiguration.proxy(request, sniUri); proxy.ifPresent(clientProxy -> ClientProxy.setBasicAuthorizationHeader(request.getHeaders(), proxy.get())); - uc = this.connectionFactory.getConnection(sniUri.toURL(), proxy.isPresent() ? proxy.get().proxy() : null); + uc = ((Supplier<HttpUrlConnectorProvider.ConnectionFactory>) requestConfiguration.connectionFactory) + .get().getConnection(sniUri.toURL(), proxy.isPresent() ? proxy.get().proxy() : null); uc.setDoInput(true); final String httpMethod = request.getMethod(); - if (request.resolveProperty(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, setMethodWorkaround)) { + if (requestConfiguration.isMethodWorkaround(request)) { setRequestMethodViaJreBugWorkaround(uc, httpMethod); } else { uc.setRequestMethod(httpMethod); } - uc.setInstanceFollowRedirects(request.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true)); + uc.setInstanceFollowRedirects(requestConfiguration.followRedirects(request)); - uc.setConnectTimeout(request.resolveProperty(ClientProperties.CONNECT_TIMEOUT, uc.getConnectTimeout())); + if (requestConfiguration.connectTimeout() == 0) { + requestConfiguration.connectTimeout(uc.getConnectTimeout()); + } + uc.setConnectTimeout(requestConfiguration.connectTimeout(request)); - uc.setReadTimeout(request.resolveProperty(ClientProperties.READ_TIMEOUT, uc.getReadTimeout())); + if (requestConfiguration.readTimeout() == 0) { + requestConfiguration.readTimeout(uc.getReadTimeout()); + } + uc.setReadTimeout(requestConfiguration.readTimeout(request).readTimeout()); - secureConnection(request, uc, sniConfig); + secureConnection(request, uc, sniConfig, requestConfiguration); final Object entity = request.getEntity(); Exception storedException = null; try { if (entity != null) { - RequestEntityProcessing entityProcessing = request.resolveProperty( - ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class); - + RequestEntityProcessing entityProcessing = requestConfiguration.requestEntityProcessing(request); final long length = request.getLengthLong(); - if (entityProcessing == null || entityProcessing != RequestEntityProcessing.BUFFERED) { - if (fixLengthStreaming && length > 0) { + if (entityProcessing != RequestEntityProcessing.BUFFERED) { + if (requestConfiguration.useFixedLengthStreaming.get() && length > 0) { uc.setFixedLengthStreamingMode(length); } else if (entityProcessing == RequestEntityProcessing.CHUNKED) { - uc.setChunkedStreamingMode(chunkSize); + uc.setChunkedStreamingMode(requestConfiguration.chunkSize.get()); } } uc.setDoOutput(true); @@ -438,13 +449,13 @@ processExtensions(request, uc); request.setStreamProvider(contentLength -> { - setOutboundHeaders(request.getStringHeaders(), uc); + setOutboundHeaders(request.getStringHeaders(), uc, requestConfiguration); return uc.getOutputStream(); }); request.writeEntity(); } else { - setOutboundHeaders(request.getStringHeaders(), uc); + setOutboundHeaders(request.getStringHeaders(), uc, requestConfiguration); } } catch (IOException ioe) { storedException = handleException(request, ioe, uc); @@ -496,7 +507,9 @@ return responseContext; } - private void setOutboundHeaders(MultivaluedMap<String, String> headers, HttpURLConnection uc) { + private void setOutboundHeaders(MultivaluedMap<String, String> headers, + HttpURLConnection uc, + HttpUrlConnectorConfiguration.ReadWrite requestConfiguration) { boolean restrictedSent = false; for (Map.Entry<String, List<String>> header : headers.entrySet()) { String headerName = header.getKey(); @@ -520,14 +533,15 @@ uc.setRequestProperty(headerName, headerValue); } // if (at least one) restricted header was added and the allowRestrictedHeaders - if (!isRestrictedHeaderPropertySet && !restrictedSent) { + if (!requestConfiguration.isRestrictedHeaderPropertySet.get() && !restrictedSent) { if (isHeaderRestricted(headerName, headerValue)) { restrictedSent = true; } } } if (restrictedSent) { - LOGGER.warning(LocalizationMessages.RESTRICTED_HEADER_POSSIBLY_IGNORED(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY)); + LOGGER.warning(LocalizationMessages.RESTRICTED_HEADER_POSSIBLY_IGNORED( + HttpUrlConnectorConfiguration.ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY)); } } @@ -635,7 +649,7 @@ private static class SniSSLSocketFactory extends SSLSocketFactory { private final SSLSocketFactory socketFactory; - private ThreadLocal<SSLParamConfigurator> sniConfigs = new ThreadLocal<>(); + private final ThreadLocal<SSLParamConfigurator> sniConfigs = new ThreadLocal<>(); public void setSniConfig(SSLParamConfigurator sniConfigs) { this.sniConfigs.set(sniConfigs);
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnectorConfiguration.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnectorConfiguration.java new file mode 100644 index 0000000..20c31f7 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnectorConfiguration.java
@@ -0,0 +1,312 @@ +/* + * Copyright (c) 2025 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.client.internal; + +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.glassfish.jersey.client.innate.ConnectorConfiguration; +import org.glassfish.jersey.internal.util.PropertiesHelper; +import org.glassfish.jersey.internal.util.collection.Ref; + +import javax.net.ssl.SSLContext; +import jakarta.ws.rs.core.Configuration; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; +import java.security.AccessController; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.logging.Logger; + +public abstract class HttpUrlConnectorConfiguration<C extends HttpUrlConnectorConfiguration<C>> + extends ConnectorConfiguration<C> { + private static final Logger LOGGER = Logger.getLogger(HttpUrlConnectorConfiguration.class.getName()); + /* package */ static final String ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY = "sun.net.http.allowRestrictedHeaders"; + /** + * Default connection factory to be used. + */ + protected static final HttpUrlConnectorProvider.ConnectionFactory DEFAULT_CONNECTION_FACTORY = + new DefaultConnectionFactory(); + + protected NullableRef<HttpUrlConnectorProvider.ConnectionFactory> connectionFactory = NullableRef.empty(); + protected Ref<Integer> chunkSize = NullableRef.empty(); + /* package */ Ref<Boolean> isRestrictedHeaderPropertySet = NullableRef.empty(); + protected Ref<Boolean> useFixedLengthStreaming = NullableRef.empty(); + protected Ref<Boolean> useSetMethodWorkaround = NullableRef.empty(); + + protected void preInit(Map<String, Object> properties) { + connectionFactory.ifEmptySet(DEFAULT_CONNECTION_FACTORY); + ((NullableRef<Integer>) chunkSize).ifEmptySet(ClientProperties.DEFAULT_CHUNK_SIZE); + ((NullableRef<Boolean>) useFixedLengthStreaming).ifEmptySet(Boolean.FALSE); + ((NullableRef<Boolean>) useSetMethodWorkaround).ifEmptySet(Boolean.FALSE); + + int computedChunkSize = ClientProperties.getValue(properties, + _prefixed(ClientProperties.CHUNKED_ENCODING_SIZE), chunkSize.get(), Integer.class); + if (computedChunkSize < 0) { + LOGGER.warning(LocalizationMessages.NEGATIVE_CHUNK_SIZE(computedChunkSize, chunkSize.get())); + } else { + chunkSize.set(computedChunkSize); + } + + useFixedLengthStreaming(ClientProperties.getValue(properties, + _prefixed(HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING), + useFixedLengthStreaming.get(), Boolean.class)); + useSetMethodWorkaround(ClientProperties.getValue(properties, + _prefixed(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND), + useSetMethodWorkaround.get(), Boolean.class)); + } + + private String _prefixed(String property) { + return prefix.ifPresentOrElse("") + property; + } + + /** + * Set a custom {@link java.net.HttpURLConnection} factory. + * + * @param connectionFactory custom HTTP URL connection factory. Must not be {@code null}. + * @return updated configuration. + * @throws java.lang.NullPointerException in case the supplied connectionFactory is {@code null}. + */ + public C connectionFactory(final HttpUrlConnectorProvider.ConnectionFactory connectionFactory) { + if (connectionFactory == null) { + throw new NullPointerException(LocalizationMessages.NULL_INPUT_PARAMETER("connectionFactory")); + } + + this.connectionFactory.set(connectionFactory); + return self(); + } + + /** + * Set chunk size for requests transferred using a + * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1">HTTP chunked transfer coding</a>. + * + * If no value is set, the default chunk size of 4096 bytes will be used. + * <p> + * Note that this programmatically set value can be overridden by + * setting the {@link org.glassfish.jersey.client.ClientProperties#CHUNKED_ENCODING_SIZE} property + * specified in the Jersey client instance configuration. + * </p> + * + * @param chunkSize chunked transfer coding chunk size to be used. + * @return updated configuration. + * @throws java.lang.IllegalArgumentException in case the specified chunk size is negative. + */ + public C chunkSize(final int chunkSize) { + if (chunkSize < 0) { + throw new IllegalArgumentException(LocalizationMessages.NEGATIVE_INPUT_PARAMETER("chunkSize")); + } + this.chunkSize.set(chunkSize); + return self(); + } + + /** + * Instruct the provided connectors to use the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int) + * fixed-length streaming mode} on the underlying HTTP URL connection instance when sending requests. + * See {@link HttpUrlConnectorProvider#USE_FIXED_LENGTH_STREAMING} property documentation for more details. + * <p> + * Note that this programmatically set value can be overridden by + * setting the {@code USE_FIXED_LENGTH_STREAMING} property specified in the Jersey client instance configuration. + * </p> + * + * @return updated configuration. + */ + public C useFixedLengthStreaming(boolean use) { + this.useFixedLengthStreaming.set(use); + return self(); + } + + /** + * Instruct the provided connectors to use reflection when setting the + * HTTP method value. See {@link HttpUrlConnectorProvider#SET_METHOD_WORKAROUND} property documentation for more details. + * <p> + * Note that this programmatically set value can be overridden by + * setting the {@code SET_METHOD_WORKAROUND} property specified in the Jersey client instance configuration + * or in the request properties. + * </p> + * + * @return updated configuration. + */ + public C useSetMethodWorkaround(boolean use) { + this.useSetMethodWorkaround.set(use); + return self(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final HttpUrlConnectorConfiguration<?> that = (HttpUrlConnectorConfiguration<?>) o; + + if (!chunkSize.equals(that.chunkSize)) { + return false; + } + if (!useFixedLengthStreaming.equals(that.useFixedLengthStreaming)) { + return false; + } + if (!useSetMethodWorkaround.equals(that.useSetMethodWorkaround)) { + return false; + } + if (!isRestrictedHeaderPropertySet.equals(that.isRestrictedHeaderPropertySet)) { + return false; + } + + return connectionFactory.equals(that.connectionFactory); + } + + @Override + public int hashCode() { + return Objects.hash(connectionFactory, chunkSize, useFixedLengthStreaming, + useSetMethodWorkaround, isRestrictedHeaderPropertySet); + } + + /* package */ ReadWrite rw() { + final ReadWrite readWrite = this instanceof ReadWrite ? ((ReadWrite) this).instance() : new ReadWrite(); + readWrite.setNonEmpty(this); + return readWrite; + } + + protected static class ReadWrite + extends HttpUrlConnectorConfiguration<ReadWrite> + implements ConnectorConfiguration.Read<ReadWrite> { + + @Override + public <X extends ConnectorConfiguration<?>> void setNonEmpty(X otherC) { + HttpUrlConnectorConfiguration<?> other = (HttpUrlConnectorConfiguration<?>) otherC; + Read.super.setNonEmpty(other); + + this.connectionFactory.setNonEmpty(other.connectionFactory); + ((NullableRef<Integer>) this.chunkSize).setNonEmpty((NullableRef<Integer>) other.chunkSize); + ((NullableRef<Boolean>) this.isRestrictedHeaderPropertySet).setNonEmpty( + (NullableRef<Boolean>) other.isRestrictedHeaderPropertySet); + ((NullableRef<Boolean>) this.useFixedLengthStreaming).setNonEmpty( + (NullableRef<Boolean>) other.useFixedLengthStreaming); + ((NullableRef<Boolean>) this.useSetMethodWorkaround).setNonEmpty((NullableRef<Boolean>) other.useSetMethodWorkaround); + } + + @Override + public ReadWrite init() { + ConnectorConfiguration.Read.super.init(); + preInit(Collections.emptyMap()); + isRestrictedHeaderPropertySet.set(Boolean.FALSE); + return self(); + } + + ReadWrite fromClient(Configuration configuration) { + ReadWrite clientConfiguration = copyFromClient(configuration); + clientConfiguration.preInit(configuration.getProperties()); + + // check if sun.net.http.allowRestrictedHeaders system property has been set and log the result + // the property is being cached in the HttpURLConnection, so this is only informative - there might + // already be some connection(s), that existed before the property was set/changed. + isRestrictedHeaderPropertySet.set(Boolean.valueOf(AccessController.doPrivileged( + PropertiesHelper.getSystemProperty(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY, "false") + ))); + + LOGGER.config(isRestrictedHeaderPropertySet.get() + ? LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_TRUE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY) + : LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_FALSE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY) + ); + + return clientConfiguration; + } + + ReadWrite fromRequest(ClientRequest request) { + ReadWrite requestConfiguration = copyFromRequest(request); + requestConfiguration.chunkSize( + request.resolveProperty(prefixed(ClientProperties.CHUNKED_ENCODING_SIZE), + requestConfiguration.chunkSize.get())); + requestConfiguration.useFixedLengthStreaming( + request.resolveProperty(prefixed(HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING), + requestConfiguration.useFixedLengthStreaming.get())); + requestConfiguration.useSetMethodWorkaround( + request.resolveProperty(prefixed(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND), + requestConfiguration.useSetMethodWorkaround.get())); + + return requestConfiguration; + } + + @Override + public ReadWrite instance() { + return new ReadWrite(); + } + + @Override + public ReadWrite me() { + return this; + } + + public boolean isMethodWorkaround(ClientRequest request) { + return request.resolveProperty( + prefixed(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND), useSetMethodWorkaround.get()); + } + + public boolean isPrefixed() { + return !prefix.get().isEmpty(); + } + + public boolean isSslContextSupplier() { + return sslContextSupplier.get() != null; + } + + /** + * Get {@link SSLContext} either from the {@link ClientProperties#SSL_CONTEXT_SUPPLIER}, or from this configuration. + * + * @param request the request used to get the {@link SSLContext}. + * @return the {@link SSLContext} supplier. + */ + public Supplier<SSLContext> sslContext(ClientRequest request) { + @SuppressWarnings("unchecked") + Supplier<SSLContext> supplier = + request.resolveProperty(prefixed(ClientProperties.SSL_CONTEXT_SUPPLIER), Supplier.class); + if (supplier == null) { + supplier = self().sslContextSupplier.get(); + } + return supplier; + } + + @Override + public ReadWrite self() { + return this; + } + } + + private static class DefaultConnectionFactory implements HttpUrlConnectorProvider.ConnectionFactory { + + @Override + public HttpURLConnection getConnection(final URL url) throws IOException { + return connect(url, null); + } + + @Override + public HttpURLConnection getConnection(URL url, Proxy proxy) throws IOException { + return connect(url, proxy); + } + + private HttpURLConnection connect(URL url, Proxy proxy) throws IOException { + return (proxy == null) ? (HttpURLConnection) url.openConnection() : (HttpURLConnection) url.openConnection(proxy); + } + } +}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java index ae487bf..9557c29 100644 --- a/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java +++ b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
@@ -51,6 +51,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -222,6 +223,43 @@ } } + @Test + public void testResponseContextCaseInsensitiveKeys() { + try (Response response = ClientBuilder.newClient() + .register(new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.abortWith(Response.ok() + .header("header1", "value") + .header("header1", "value1 , value2") + .header("header1", "Value3,white space ") + .header("header2", "Value4;;Value5") + .build()); + } + }) + .register(new ClientResponseFilter() { + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext context) { + Assertions.assertTrue(context.getHeaderString("header1").contains("value")); + Assertions.assertTrue(context.getHeaderString("HEADER1").contains("value2")); + //White space in value not trimmed + Assertions.assertFalse(context.getHeaderString("header1").contains("whitespace")); + //Multiple character separator + Assertions.assertTrue(context.getHeaderString("header2").contains("Value5")); + + Assertions.assertTrue(context.getHeaders().containsKey("HEADer1")); + Assertions.assertFalse(context.getHeaders().get("HEADer1").isEmpty()); + Assertions.assertFalse(context.getHeaders().remove("HeAdEr1").isEmpty()); + Assertions.assertFalse(context.getHeaders().containsKey("header1")); + } + }) + .target("http://localhost:8080") + .request() + .get()) { + Assertions.assertEquals(200, response.getStatus()); + } + } + private static class StringHeader extends AtomicReference<String> { }
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorConfigurationTest.java b/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorConfigurationTest.java new file mode 100644 index 0000000..5e31e48 --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorConfigurationTest.java
@@ -0,0 +1,793 @@ +/* + * Copyright (c) 2025 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.client; + +import org.glassfish.jersey.client.innate.ClientProxy; +import org.glassfish.jersey.client.innate.ConnectorConfiguration; +import org.glassfish.jersey.client.internal.HttpUrlConnector; +import org.glassfish.jersey.client.internal.HttpUrlConnectorConfiguration; +import org.glassfish.jersey.http.HttpHeaders; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.internal.util.JdkVersion; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MultivaluedMap; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ProtocolException; +import java.net.URI; +import java.net.URL; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class HttpUrlConnectorConfigurationTest { + + public static final String PREFIX = "test."; + + @Test + public void testConnectTimeout() { + final AtomicInteger result = new AtomicInteger(0); + class RWConnect extends Conf.RW { + @Override + public int connectTimeout(ClientRequest request) { + result.set(super.connectTimeout(request)); + throw new IllegalStateException(); + } + + @Override + public RWConnect instance() { + return new RWConnect(); + } + + } + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)).request().apply(); + Assertions.assertEquals(1000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECT_TIMEOUT, 3000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request().apply(); + Assertions.assertEquals(3000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECT_TIMEOUT, 3000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWConnect().connectTimeout(4000))) + .apply(); + Assertions.assertEquals(3000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWConnect().connectTimeout(4000))) + .apply(); + Assertions.assertEquals(4000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWConnect().connectTimeout(4000))) + .request(r -> r.setProperty(ClientProperties.CONNECT_TIMEOUT, 5000)) + .apply(); + Assertions.assertEquals(5000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().connectTimeout(2000).prefix(PREFIX))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWConnect().connectTimeout(4000))) + .apply(); + Assertions.assertEquals(2000, result.get()); + + result.set(0); + new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().connectTimeout(4000).prefix(PREFIX))) + .request(r -> r.setProperty(ClientProperties.CONNECT_TIMEOUT, 5000)) + .apply(); + Assertions.assertEquals(4000, result.get()); + } + + @Test + public void testFollowRedirects() { + final AtomicReference<Boolean> result = new AtomicReference<>(); + class RWFollow extends Conf.RW { + + @Override + public boolean followRedirects(ClientRequest request) { + result.set(super.followRedirects(request)); + throw new IllegalStateException(); + } + + @Override + public RWFollow instance() { + return new RWFollow(); + } + } + + Request req; + req = new TestClient(new RWFollow()).request().apply(); + Assertions.assertEquals(((RWFollow) new RWFollow().copy()).followRedirects(), result.get()); + + result.set(null); + req = new TestClient(new RWFollow().followRedirects(false)).request().apply(); + Assertions.assertEquals(false, result.get()); + + result.set(null); + req = new TestClient(new RWFollow().prefix(PREFIX).followRedirects(false)) + .request(r -> r.setProperty(PREFIX + ClientProperties.FOLLOW_REDIRECTS, true)) + .apply(); + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWFollow().prefix(PREFIX).followRedirects(false)) + .request(r -> r.setProperty( + PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWFollow().followRedirects(true))) + .apply(); + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWFollow()) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWFollow().followRedirects(false))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWFollow().followRedirects(true))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(true, result.get()); + + result.set(null); + req = new TestClient(new RWFollow().prefix(PREFIX)) + .client(c -> c.property(PREFIX + ClientProperties.FOLLOW_REDIRECTS, false)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + new RWFollow().followRedirects(true))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(false, result.get()); + } + + @Test + public void testProxy() { + final AtomicReference<ClientProxy> result = new AtomicReference<>(); + class RWProxy extends Conf.RW { + + @Override + public Optional<ClientProxy> proxy(ClientRequest request, URI requestUri) { + Optional<ClientProxy> proxy = super.proxy(request, requestUri); + result.set(proxy.orElse(null)); + throw new IllegalStateException(); + } + + @Override + public RWProxy instance() { + return new RWProxy(); + } + } + String proxyUri = "http://proxy.org:8080"; + String userName = "USERNAME"; + String password = "PASSWORD"; + + new TestClient(new RWProxy()).request().apply(); + Assertions.assertNull(result.get()); + + result.set(null); + new TestClient(new RWProxy()).rw(rw -> rw.proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)) + .request().apply(); + Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString()); + Assertions.assertEquals(userName, result.get().userName()); + Assertions.assertEquals(password, result.get().password()); + + result.set(null); + new TestClient(new RWProxy()).rw(rw -> rw.prefix(PREFIX).proxyUri(proxyUri).proxyUserName("XXX").proxyPassword(password)) + .request(r -> r.setProperty(PREFIX + ClientProperties.PROXY_USERNAME, userName)) + .request(r -> r.setProperty(ClientProperties.PROXY_PASSWORD, "XXX")) + .apply(); + Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString()); + Assertions.assertEquals(userName, result.get().userName()); + Assertions.assertEquals(password, result.get().password()); + + result.set(null); + new TestClient(new RWProxy()).rw(rw -> rw.prefix(PREFIX).proxyUri(proxyUri).proxyUserName("XXX").proxyPassword(password)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().proxyUserName(userName).proxyPassword(null))) + .apply(); + Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString()); + Assertions.assertEquals(userName, result.get().userName()); + Assertions.assertNull(result.get().password()); + + result.set(null); + new TestClient(new RWProxy().prefix(PREFIX) + .proxy(new java.net.Proxy(java.net.Proxy.Type.HTTP, + new InetSocketAddress(proxyUri.split("g:")[0] + "g", Integer.parseInt(proxyUri.split("g:")[1]))))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().proxyUserName(userName).proxyPassword(password).prefix(PREFIX))) + .apply(); + Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString()); + Assertions.assertEquals(userName, result.get().userName()); + Assertions.assertEquals(password, result.get().password()); + } + + @Test + public void testReadTimeout() { + final AtomicInteger result = new AtomicInteger(0); + class RWRead extends Conf.RW { + + @Override + public RWRead readTimeout(ClientRequest clientRequest) { + super.readTimeout(clientRequest); + result.set(readTimeout()); + throw new IllegalStateException(); + } + + @Override + public RWRead instance() { + return new RWRead(); + } + } + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)).request().apply(); + Assertions.assertEquals(1000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)) + .client(c -> c.property(ClientProperties.READ_TIMEOUT, 3000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000))) + .request().apply(); + Assertions.assertEquals(3000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)) + .client(c -> c.property(ClientProperties.READ_TIMEOUT, 3000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().connectTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000))) + .apply(); + Assertions.assertEquals(3000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000))) + .apply(); + Assertions.assertEquals(4000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000))) + .request(r -> r.setProperty(ClientProperties.READ_TIMEOUT, 5000)) + .apply(); + Assertions.assertEquals(5000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().readTimeout(2000).prefix(PREFIX))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWRead().readTimeout(4000))) + .apply(); + Assertions.assertEquals(2000, result.get()); + + result.set(0); + new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000).prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000))) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().readTimeout(4000).prefix(PREFIX))) + .request(r -> r.setProperty(ClientProperties.READ_TIMEOUT, 5000)) + .apply(); + Assertions.assertEquals(4000, result.get()); + } + + @Test + public void testRequestEntityProcessing() { + final AtomicReference<RequestEntityProcessing> result = new AtomicReference<>(); + final AtomicReference<Conf.RW> config = new AtomicReference<>(); + class RWRP extends Conf.RW { + + @Override + public RequestEntityProcessing requestEntityProcessing(ClientRequest request) { + result.set(super.requestEntityProcessing(request)); + throw new IllegalStateException(); + } + + @Override + public RWRP instance() { + config.set(new RWRP()); + return (RWRP) config.get(); + } + } + + Request req; + req = new TestClient(new RWRP().requestEntityProcessing(RequestEntityProcessing.CHUNKED)) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(RequestEntityProcessing.CHUNKED, result.get()); + + result.set(null); + req = new TestClient(new RWRP().prefix(PREFIX).requestEntityProcessing(RequestEntityProcessing.CHUNKED)) + .request(r -> r.setEntity(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config() + .requestEntityProcessing(RequestEntityProcessing.BUFFERED).prefix(PREFIX))) + .apply(); + Assertions.assertEquals(RequestEntityProcessing.BUFFERED, result.get()); + + result.set(null); + req = new TestClient(new RWRP().prefix(PREFIX).requestEntityProcessing(RequestEntityProcessing.CHUNKED)) + .request(r -> r.setEntity(PREFIX)) + .request(r -> r.setProperty(PREFIX + ClientProperties.REQUEST_ENTITY_PROCESSING, + RequestEntityProcessing.BUFFERED)) + .apply(); + Assertions.assertEquals(RequestEntityProcessing.BUFFERED, result.get()); + } + + @Test + public void testSslContext() { + final SSLContext testContext = new SSLContext(null, null, null){}; + final AtomicReference<SSLContext> result = new AtomicReference<>(); + final AtomicReference<Conf.RW> config = new AtomicReference<>(); + class RWSsl extends Conf.RW { + + @Override + public SSLContext sslContext(Client client, ClientRequest request) { + result.set(super.sslContext(client, request)); + return result.get(); + } + + @Override + public boolean isSslContextSupplier() { + throw new IllegalStateException(); + } + + @Override + public RWSsl instance() { + RWSsl rw = new RWSsl(); + config.set(rw); + return rw; + } + } + + Request req; + req = new TestClient(new RWSsl().sslContextSupplier(() -> testContext)).request().apply(); + config.get().sslContext(req.client, req.request); + Assertions.assertEquals(testContext, result.get()); + + result.set(null); + req = new TestClient(new RWSsl().prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + ClientProperties.SSL_CONTEXT_SUPPLIER, + (Supplier<SSLContext>) () -> testContext)).apply(); + config.get().sslContext(req.client, req.request); + Assertions.assertEquals(testContext, result.get()); + + result.set(null); + req = new TestClient(new RWSsl().prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + new RWSsl().prefix(PREFIX).sslContextSupplier(() -> testContext))).apply(); + config.get().sslContext(req.client, req.request); + Assertions.assertEquals(testContext, result.get()); + } + + @Test + public void testConnectionFactory() { + final AtomicReference<Boolean> result = new AtomicReference<>(); + final HttpUrlConnectorProvider.ConnectionFactory connectionFactory = new HttpUrlConnectorProvider.ConnectionFactory() { + @Override + public HttpURLConnection getConnection(URL url) throws IOException { + result.set(true); + throw new IllegalStateException(); + } + }; + + new TestClient(HttpUrlConnectorProvider.config().connectionFactory(connectionFactory)).request().apply(); + Assertions.assertTrue(result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().connectionFactory(connectionFactory))) + .request().apply(); + Assertions.assertNull(result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().connectionFactory(connectionFactory).prefix(PREFIX))) + .request().apply(); + Assertions.assertTrue(result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().connectionFactory(connectionFactory).prefix(PREFIX))) + .apply(); + Assertions.assertTrue(result.get()); + + result.set(null); + Client client = ClientBuilder.newClient(); + try { + HttpUrlConnectorProvider.config().build() + .connectionFactory(connectionFactory) + .getConnector(client, client.getConfiguration()).apply(Request.createRequest(client)); + } catch (IllegalStateException ise) { + // expected + } + Assertions.assertTrue(result.get()); + } + + @Test + public void testChunkSize() { + final AtomicReference<Integer> result = new AtomicReference<>(); + final HttpUrlConnectorProvider.ConnectionFactory connectionFactory = new HttpUrlConnectorProvider.ConnectionFactory() { + @Override + public HttpURLConnection getConnection(URL url) throws IOException { + return new HttpURLConnection(url) { + @Override + public void disconnect() { + + } + + @Override + public boolean usingProxy() { + return false; + } + + @Override + public void connect() throws IOException { + + } + + @Override + public void setChunkedStreamingMode(int chunklen) { + result.set(chunklen); + throw new IllegalStateException(); + } + }; + } + }; + + new TestClient(HttpUrlConnectorProvider.config() + .requestEntityProcessing(RequestEntityProcessing.CHUNKED) + .connectionFactory(connectionFactory)) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(ClientProperties.DEFAULT_CHUNK_SIZE, result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .requestEntityProcessing(RequestEntityProcessing.CHUNKED) + .connectionFactory(connectionFactory) + .chunkSize(1000)) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(1000, result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .requestEntityProcessing(RequestEntityProcessing.CHUNKED) + .connectionFactory(connectionFactory) + .chunkSize(1000) + .prefix(PREFIX)) + .client(c -> c.property(PREFIX + ClientProperties.CHUNKED_ENCODING_SIZE, 2000)) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(2000, result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .requestEntityProcessing(RequestEntityProcessing.CHUNKED) + .connectionFactory(connectionFactory) + .chunkSize(1000) + .prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + ClientProperties.CHUNKED_ENCODING_SIZE, 2000)) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(2000, result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .requestEntityProcessing(RequestEntityProcessing.CHUNKED) + .connectionFactory(connectionFactory) + .chunkSize(1000) + .prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().chunkSize(2000).prefix(PREFIX))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(2000, result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .requestEntityProcessing(RequestEntityProcessing.CHUNKED) + .connectionFactory(connectionFactory) + .chunkSize(1000) + .prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().chunkSize(2000).prefix(PREFIX))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(2000, result.get()); + } + + @Test + public void testUseFixedLengthStreaming() { + final AtomicReference<Integer> result = new AtomicReference<>(); + final class FixedLengthStreamingUrlConnection extends HttpURLConnection { + private FixedLengthStreamingUrlConnection(URL u) { + super(u); + } + + @Override + public void disconnect() { + + } + + @Override + public boolean usingProxy() { + return false; + } + + @Override + public void connect() throws IOException { + + } + + @Override + public void setFixedLengthStreamingMode(long contentLength) { + super.setFixedLengthStreamingMode(contentLength); + result.set((int) contentLength); + throw new IllegalStateException(); + } + } + final HttpUrlConnectorProvider.ConnectionFactory connectionFactory = new HttpUrlConnectorProvider.ConnectionFactory() { + @Override + public HttpURLConnection getConnection(URL url) throws IOException { + return new FixedLengthStreamingUrlConnection(url); + } + }; + + new TestClient(HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory)) + .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length()))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertNull(result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory) + .useFixedLengthStreaming(true)) + .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length()))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(PREFIX.length(), result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory) + .useFixedLengthStreaming(false) + .prefix(PREFIX)) + .client(c -> c.property(PREFIX + HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING, true)) + .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length()))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(PREFIX.length(), result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory) + .useFixedLengthStreaming(false) + .prefix(PREFIX)) + .request(r -> r.setProperty(PREFIX + HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING, true)) + .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length()))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(PREFIX.length(), result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory) + .useFixedLengthStreaming(false) + .prefix(PREFIX)) + .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().useFixedLengthStreaming(true).prefix(PREFIX))) + .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length()))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(PREFIX.length(), result.get()); + + result.set(null); + new TestClient(HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory) + .useFixedLengthStreaming(false) + .prefix(PREFIX)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().useFixedLengthStreaming(true).prefix(PREFIX))) + .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length()))) + .request(r -> r.setEntity(PREFIX)).apply(); + Assertions.assertEquals(PREFIX.length(), result.get()); + } + + @Test + public void testUseMethodWorkaround() { + JdkVersion version = JdkVersion.getJdkVersion(); + if (!version.isReflectiveAccessToJdkSupported()) { + return; + } + final String method = "XYZ"; + final AtomicReference<HttpURLConnection> result = new AtomicReference<>(); + final HttpUrlConnectorProvider.ConnectionFactory connectionFactory = new HttpUrlConnectorProvider.ConnectionFactory() { + @Override + public HttpURLConnection getConnection(URL url) throws IOException { + return new HttpURLConnection(url) { + @Override + public void disconnect() { + + } + + @Override + public boolean usingProxy() { + return false; + } + + @Override + public void connect() throws IOException { + + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + result.set(this); + super.setRequestMethod(method); + throw new IllegalStateException(); + } + }; + } + }; + + new TestClient(HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory)) + .request(r -> r.setMethod(method)) + .apply(); + Assertions.assertNotEquals(method, result.get().getRequestMethod()); + + new TestClient(HttpUrlConnectorProvider.config() + .connectionFactory(connectionFactory) + .useSetMethodWorkaround(true)) + .request(r -> r.setMethod(method)) + .apply(); + Assertions.assertEquals(method, result.get().getRequestMethod()); + + HttpUrlConnectorProvider.Config config = HttpUrlConnectorProvider.config(); + config.build().useSetMethodWorkaround(); + new TestClient(config + .connectionFactory(connectionFactory)) + .request(r -> r.setMethod(method)) + .apply(); + Assertions.assertEquals(method, result.get().getRequestMethod()); + + new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX) + .connectionFactory(connectionFactory)) + .request(r -> r.setMethod(method)) + .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, + HttpUrlConnectorProvider.config().useSetMethodWorkaround(true).prefix(PREFIX))) + .apply(); + Assertions.assertEquals(method, result.get().getRequestMethod()); + + new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX) + .connectionFactory(connectionFactory)) + .client(c -> c.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)) + .request(r -> r.setMethod(method)) + .apply(); + Assertions.assertNotEquals(method, result.get().getRequestMethod()); + + new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX) + .connectionFactory(connectionFactory)) + .client(c -> c.property(PREFIX + HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)) + .request(r -> r.setMethod(method)) + .apply(); + Assertions.assertEquals(method, result.get().getRequestMethod()); + } + + private static class Conf extends HttpUrlConnectorConfiguration<Conf> { + private static class RW extends ReadWrite { + @Override + public <X extends ConnectorConfiguration<?>> void setNonEmpty(X otherC) { + super.setNonEmpty(otherC); + } + + @Override + public RW prefix(String prefix) { + super.prefix(prefix); + return this; + } + } + } + + private static class TestClient { + final Conf.RW rw; + final Client client; + + private TestClient(ConnectorConfiguration<?> config) { + this.rw = config instanceof Conf.RW ? (Conf.RW) config : new Conf.RW() { + { + setNonEmpty(config); + } + }; + this.client = ClientBuilder.newClient(new ClientConfig()); + } + + public TestClient client(Consumer<Client> consumer) { + consumer.accept(client); + return this; + } + + public TestClient rw(Consumer<HttpUrlConnectorConfiguration<?>> consumer) { + consumer.accept(rw); + return this; + } + + public Request request() { + return new Request(client, rw); + } + + public Request request(Consumer<ClientRequest> consumer) { + return request().request(consumer); + } + } + + private static class Request { + final ClientRequest request; + final Client client; + final Conf.RW rw; + + Request(Client client, Conf.RW rw) { + this.client = client; + this.rw = rw; + request = createRequest(client); + } + + private static ClientRequest createRequest(Client client) { + ClientRequest request = new ClientRequest(URI.create("http://localhost:8080"), + (ClientConfig) client.getConfiguration(), new MapPropertiesDelegate()) { + @Override + public void writeEntity() throws IOException { + throw new IllegalStateException(); + } + + @Override + public MultivaluedMap<String, String> getStringHeaders() { + throw new IllegalStateException(); + } + }; + request.setMethod("POST"); + return request; + } + + public Request request(Consumer<ClientRequest> consumer) { + consumer.accept(request); + return this; + } + + public Request apply() { + try { + HttpUrlConnector connector = new HttpUrlConnector(client, client.getConfiguration(), rw); + connector.apply(request); + } catch (ProcessingException | IllegalStateException expected) { + } + return this; + } + } +}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java index 7b83560..8390bcf 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Payara Foundation and/or its affiliates. * * This program and the accompanying materials are made available under the @@ -326,7 +326,8 @@ final List<ClassTypePair> ctps = ReflectionHelper.getTypeArgumentAndClass(genericType); final ClassTypePair ctp = (ctps.size() == 1) ? ctps.get(0) : null; final boolean empty = value.isEmpty(); - for (ParamConverterProvider provider : Providers.getProviders(manager, ParamConverterProvider.class)) { + for (final ParamConverterProvider provider + : Providers.getAllProviders(manager, ParamConverterProvider.class)) { final ParamConverter<?> converter = provider.getConverter(ctp.rawClass(), ctp.type(), annotations); if (converter != null) { if (empty) {
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/JdkVersion.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/JdkVersion.java index 9b8ec40..d7c478c 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/util/JdkVersion.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/JdkVersion.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025 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 @@ -119,6 +119,19 @@ } /** + * <p> + * Returns <tt>true</tt> if the reflective access to JDK classes is supported. It was disabled by JDK 16+. + * </p> + * <p> + * This can be used to verify the property <tt>HttpUrlConnectorProvider#SET_METHOD_WORKAROUND</tt> still works, + * for instance. + * </p> + */ + public boolean isReflectiveAccessToJdkSupported() { + return getMajor() < 16; + } + + /** * Returns <tt>true</tt> if {@code sun.misc.Unsafe} is present in the * current JDK version, or <tt>false</tt> otherwise. *
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java index 75b1987..d1480d2 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
@@ -230,6 +230,76 @@ final List<Object> old = originalMap.put(key, (List<Object>) (List<?>) value); return valuesTransformer.apply(old); } + + @Override + public boolean containsKey(Object key) { + Iterator<Entry<String, List<String>>> i = entrySet().iterator(); + if (key == null) { + while (i.hasNext()) { + Entry<String, List<String>> e = i.next(); + if (e.getKey() == null) { + return true; + } + } + } else { + while (i.hasNext()) { + Entry<String, List<String>> e = i.next(); + if (((String) key).equalsIgnoreCase(e.getKey())) { + return true; + } + } + } + return false; + } + + @Override + public List<String> get(Object key) { + Iterator<Entry<String, List<String>>> i = entrySet().iterator(); + if (key == null) { + while (i.hasNext()) { + Entry<String, List<String>> e = i.next(); + if (e.getKey() == null) { + return e.getValue(); + } + } + } else { + while (i.hasNext()) { + Entry<String, List<String>> e = i.next(); + if (((String) key).equalsIgnoreCase(e.getKey())) { + return e.getValue(); + } + } + } + return null; + } + + @Override + public List<String> remove(Object key) { + Iterator<Entry<String, List<String>>> i = entrySet().iterator(); + Entry<String, List<String>> correctEntry = null; + if (key == null) { + while (correctEntry == null && i.hasNext()) { + Entry<String, List<String>> e = i.next(); + if (e.getKey() == null) { + correctEntry = e; + } + } + } else { + while (correctEntry == null && i.hasNext()) { + Entry<String, List<String>> e = i.next(); + if (((String) key).equalsIgnoreCase(e.getKey())) { + correctEntry = e; + } + } + } + + List<String> oldValue = null; + if (correctEntry != null) { + oldValue = correctEntry.getValue(); + i.remove(); + } + return oldValue; + } } /**
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java b/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java index 04d820d..4e94366 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025 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 @@ -720,7 +720,7 @@ // output streams writes out bytes only on close (for example GZipOutputStream). response.close(); } catch (final Exception e) { - LOGGER.log(Level.SEVERE, LocalizationMessages.ERROR_CLOSING_COMMIT_OUTPUT_STREAM(), e); + LOGGER.log(Level.FINER, LocalizationMessages.ERROR_CLOSING_COMMIT_OUTPUT_STREAM(), e); } } }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java b/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java index 7cf46ee..1a8f1b4 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025 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 @@ -21,7 +21,6 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -280,13 +279,8 @@ model.getPriority(ContainerResponseFilter.class))); } } - - _readerInterceptors.addAll( - StreamSupport.stream(processingProviders.getGlobalReaderInterceptors().spliterator(), false) - .collect(Collectors.toList())); - _writerInterceptors.addAll( - StreamSupport.stream(processingProviders.getGlobalWriterInterceptors().spliterator(), false) - .collect(Collectors.toList())); + processingProviders.getGlobalReaderInterceptors().forEach(_readerInterceptors::add); + processingProviders.getGlobalWriterInterceptors().forEach(_writerInterceptors::add); if (resourceMethod != null) { addNameBoundFiltersAndInterceptors( @@ -458,9 +452,7 @@ if (entityAnn.length == 0) { response.setEntityAnnotations(methodAnnotations); } else { - final Annotation[] mergedAnn = Arrays.copyOf(methodAnnotations, - methodAnnotations.length + entityAnn.length); - System.arraycopy(entityAnn, 0, mergedAnn, methodAnnotations.length, entityAnn.length); + final Annotation[] mergedAnn = mergeDistinctAnnotations(methodAnnotations, entityAnn); response.setEntityAnnotations(mergedAnn); } } @@ -487,6 +479,22 @@ return jaxrsResponse; } + private static Annotation[] mergeDistinctAnnotations(Annotation[] existing, Annotation[] newOnes) { + List<Annotation> merged = new ArrayList<>(existing.length + newOnes.length); + Collections.addAll(merged, existing); + + newOnesLoop: + for (Annotation n : newOnes) { + for (Annotation ex : existing) { + if (ex == n) { + continue newOnesLoop; + } + } + merged.add(n); + } + return merged.toArray(new Annotation[0]); + } + private Type unwrapInvocableResponseType(ContainerRequest request, Type entityType) { if (isCompletionStageResponseType && request.resolveProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)) {
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java index 8413a07..4ee091b 100644 --- a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java +++ b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Payara Foundation and/or its affiliates. * * This program and the accompanying materials are made available under the @@ -27,8 +27,11 @@ import java.util.Date; import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -104,6 +107,15 @@ assertEquals(404, responseContext.getStatus()); } + @Test + public void testCustomEnumResource() throws ExecutionException, InterruptedException { + initiateWebApplication(BadEnumResource.class, EnumParamConverterProvider.class); + final ContainerResponse responseContext = getResponseContext(UriBuilder.fromPath("/") + .queryParam("d", "A").build().toString()); + assertEquals(1, counter.get()); + assertEquals(200, responseContext.getStatus()); + } + public static class URIStringReaderProvider implements ParamConverterProvider { @Override @@ -188,6 +200,40 @@ } } + static final AtomicInteger counter = new AtomicInteger(0); + @Singleton + @Priority(1) + public static class EnumParamConverterProvider implements ParamConverterProvider { + + @Override + public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) { + if (Enum.class.isAssignableFrom(rawType)) { + return new ParamConverter<T>() { + @Override + public T fromString(final String value) { + counter.addAndGet(1); + if (value == null) { + return null; + } + Class<? extends Enum> enumClass = null; + try { + enumClass = (Class<Enum>) Class.forName(genericType.getTypeName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + return (T) Enum.valueOf(enumClass, value.toUpperCase()); + } + + @Override + public String toString(final T value) { + return String.valueOf(value); + } + }; + } + return null; + } + } + @Path("/") public static class ListOfStringResource {
diff --git a/examples/NOTICE.md b/examples/NOTICE.md index 70c3d2b..0abdafc 100644 --- a/examples/NOTICE.md +++ b/examples/NOTICE.md
@@ -71,7 +71,7 @@ * Project: http://www.javassist.org/ * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. -Jackson JAX-RS Providers Version 2.18.0 +Jackson JAX-RS Providers Version 2.19.1 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.
diff --git a/examples/osgi-helloworld-webapp/functional-test/src/test/resources/felix.policy b/examples/osgi-helloworld-webapp/functional-test/src/test/resources/felix.policy index d8348f9..51a40bb 100644 --- a/examples/osgi-helloworld-webapp/functional-test/src/test/resources/felix.policy +++ b/examples/osgi-helloworld-webapp/functional-test/src/test/resources/felix.policy
@@ -1,5 +1,5 @@ // -// Copyright (c) 2014, 2020 Oracle and/or its affiliates. All rights reserved. +// Copyright (c) 2014, 2025 Oracle and/or its affiliates. All rights reserved. // // This program and the accompanying materials are made available under the // terms of the Eclipse Distribution License v. 1.0, which is available at @@ -33,6 +33,7 @@ allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (org.osgi.framework.PackagePermission) } "packagePermissionToJerseyCommon"; allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.lang.reflect.ReflectPermission "suppressAccessChecks") } "suppressAccessChecksToJerseyCommon"; allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.net.SocketPermission "*" "connect,resolve") } "socketPermissionToJerseyCommon"; +allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.net.NetPermission "*" "getProxySelector") } "proxySelectorToClientProxy"; allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.lang.RuntimePermission "accessDeclaredMembers") } "accessDeclaredMembersToJerseyCommon"; allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.lang.RuntimePermission "accessClassInPackage.sun.misc") } "accessClassInPackageSunMisc"; allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.lang.RuntimePermission "getClassLoader") } "getCLToJerseyCommon";
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java index 350be25..d86fa8b 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java
@@ -6,7 +6,10 @@ import java.io.Reader; import java.io.Writer; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Type; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -18,9 +21,12 @@ import jakarta.ws.rs.core.NoContentException; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.StreamingOutput; +import jakarta.ws.rs.ext.ContextResolver; import jakarta.ws.rs.ext.MessageBodyReader; import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Providers; +import com.fasterxml.jackson.core.PrettyPrinter; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.AnnotationBundleKey; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.Annotations; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.EndpointConfigBase; @@ -216,12 +222,13 @@ protected ProviderBase() { this(null); } + /** * @since 2.17 */ protected ProviderBase(MAPPER_CONFIG mconfig, - LookupCache<AnnotationBundleKey, EP_CONFIG> readerCache, - LookupCache<AnnotationBundleKey, EP_CONFIG> writerCache) + LookupCache<AnnotationBundleKey, EP_CONFIG> readerCache, + LookupCache<AnnotationBundleKey, EP_CONFIG> writerCache) { _mapperConfig = mconfig; _jaxRSFeatures = JAXRS_FEATURE_DEFAULTS; @@ -612,7 +619,12 @@ try { // Want indentation? if (writer.isEnabled(SerializationFeature.INDENT_OUTPUT)) { - g.useDefaultPrettyPrinter(); + PrettyPrinter defaultPrettyPrinter = writer.getConfig().getDefaultPrettyPrinter(); + if (defaultPrettyPrinter != null) { + g.setPrettyPrinter(defaultPrettyPrinter); + } else { + g.useDefaultPrettyPrinter(); + } } JavaType rootType = null; @@ -999,6 +1011,24 @@ /********************************************************** */ + // @since 2.19 + protected <OM extends ObjectMapper> OM _locateMapperViaProvider(Class<?> type, MediaType mediaType, + Class<OM> mapperType, Providers providers) { + if (providers != null) { + ContextResolver<OM> resolver = providers.getContextResolver(mapperType, mediaType); + // Above should work as is, but due to this bug + // [https://jersey.dev.java.net/issues/show_bug.cgi?id=288] + // in Jersey, it doesn't. But this works until resolution of the issue: + if (resolver == null) { + resolver = providers.getContextResolver(mapperType, null); + } + if (resolver != null) { + return resolver.getContext(type); + } + } + return null; + } + protected static boolean _containedIn(Class<?> mainType, HashSet<ClassKey> set) { if (set != null) {
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JacksonJsonProvider.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JacksonJsonProvider.java index 13f9275..93a5192 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JacksonJsonProvider.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JacksonJsonProvider.java
@@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.json.JsonMapper; /** * Basic implementation of JAX-RS abstractions ({@link MessageBodyReader}, @@ -207,21 +208,12 @@ @Override protected ObjectMapper _locateMapperViaProvider(Class<?> type, MediaType mediaType) { - if (_providers != null) { - ContextResolver<ObjectMapper> resolver = _providers.getContextResolver(ObjectMapper.class, mediaType); - /* Above should work as is, but due to this bug - * [https://jersey.dev.java.net/issues/show_bug.cgi?id=288] - * in Jersey, it doesn't. But this works until resolution of - * the issue: - */ - if (resolver == null) { - resolver = _providers.getContextResolver(ObjectMapper.class, null); - } - if (resolver != null) { - return resolver.getContext(type); - } + // 26-Nov-2024, tatu: [jakarta-rs#36] Look for JsonMapper primarily + ObjectMapper m =_locateMapperViaProvider(type, mediaType, JsonMapper.class, _providers); + if (m == null) { + m = _locateMapperViaProvider(type, mediaType, ObjectMapper.class, _providers); } - return null; + return m; } @Override
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java index fd5d9aa..3325c5e 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java
@@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; +import com.fasterxml.jackson.databind.json.JsonMapper; /** * Helper class used to encapsulate details of configuring an @@ -49,7 +50,7 @@ _lock.lock(); try { if (_defaultMapper == null) { - _defaultMapper = new ObjectMapper(); + _defaultMapper = new JsonMapper(); _setAnnotations(_defaultMapper, _defaultAnnotationsToUse); } } finally { @@ -77,7 +78,7 @@ _lock.lock(); try { if (_mapper == null) { - _mapper = new ObjectMapper(); + _mapper = new JsonMapper(); _setAnnotations(_mapper, _defaultAnnotationsToUse); } } finally { @@ -120,9 +121,8 @@ case JACKSON: return new JacksonAnnotationIntrospector(); case JAXB: - /* For this, need to use indirection just so that error occurs - * when we get here, and not when this class is being loaded - */ + // For this, need to use indirection just so that error occurs + // when we get here, and not when this class is being loaded try { if (_jaxbIntrospectorClass == null) { _jaxbIntrospectorClass = JakartaXmlBindAnnotationIntrospector.class;
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java index 44c9073..a7c123c 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java
@@ -11,7 +11,7 @@ */ public final class PackageVersion implements Versioned { public final static Version VERSION = VersionUtil.parseVersion( - "2.18.0", "com.fasterxml.jackson.jaxrs", "jackson-jaxrs-json-provider"); + "2.19.1", "com.fasterxml.jackson.jaxrs", "jackson-jaxrs-json-provider"); @Override public Version version() {
diff --git a/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown b/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown index d893c65..d9b20ee 100644 --- a/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown +++ b/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown
@@ -31,7 +31,7 @@ ## Third-party Content -Jackson JAX-RS Providers version 2.18.0 +Jackson JAX-RS Providers version 2.19.1 * License: Apache License, 2.0 * Project: https://github.com/FasterXML/jackson-jaxrs-providers * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.
diff --git a/pom.xml b/pom.xml index f1fa3ef..ee38b9c 100644 --- a/pom.xml +++ b/pom.xml
@@ -2244,7 +2244,7 @@ <hk2.jvnet.osgi.version>org.jvnet.hk2.*;version="[2.5,4)"</hk2.jvnet.osgi.version> <httpclient.version>4.5.14</httpclient.version> <httpclient5.version>5.3.1</httpclient5.version> - <jackson.version>2.18.0</jackson.version> + <jackson.version>2.19.1</jackson.version> <javassist.version>3.30.2-GA</javassist.version> <jboss.logging.8.version>3.4.3.Final</jboss.logging.8.version> <jersey1.version>1.19.3</jersey1.version>
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java new file mode 100644 index 0000000..78f1ef4 --- /dev/null +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java
@@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.api; + +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Check the reused response does not leak by additional Annotations (ContainerResponse#setEntityAnnottaions. + */ +public class Jersey5939Test extends JerseyTest { + + private final List<ContainerResponse> capturedResponses = new ArrayList<>(); + + @Override + protected Application configure() { + return new ResourceConfig(Restlet.class).register(new ContainerResponseFilter() { + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (responseContext instanceof ContainerResponse) { + capturedResponses.add((ContainerResponse) responseContext); + } + } + }); + } + + @Test + public void testIssue5939() { + for (int i = 0; i < 10; i++) { + Response response = target("foo/bar").request().get(); + assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); + ContainerResponse containerResponse = capturedResponses.get(i); + Annotation[] annotations = containerResponse.getEntityAnnotations(); + // [@javax.ws.rs.GET(), @javax.ws.rs.Path("/bar")] + assertEquals(2, annotations.length, "Found " + annotations.length + " annotations, in iteration " + i); + } + } + + @Path("/foo") + public static class Restlet { + + private static final Response RESPONSE_204 = Response.noContent().build(); + + @GET + @Path("/bar") + public Response fooBar() { + return RESPONSE_204; + } + } +} \ No newline at end of file