Skip to content

Commit

Permalink
[4.x] Modifies Helidon Connector to use WebClient and also support HT…
Browse files Browse the repository at this point in the history
…TP/2 (#7582)

* Modifies Helidon connector to use WebClient and thus support HTTP/1.1 and HTTP/2. Fixes problem in http2 module-info.java. Updates tests to verify HTTP/2 using ALPN and prior knowledge.

* - New test for protocol upgrade without TLS
- Fixes problem in connector when accessing request properties
- Renames test classes for clarity

* Cleans up support for Tls and SSLContext.

* Fixes import after merge.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>

* Fixes problems with timeouts.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>

* Fixes checkstyle problems.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>

* Enables headers test.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>

* Turns off connection cache but allows enabling this with newly defined property. Minor cleanup in pom file.

* Adds a create() method to HelidonConnectorProvider. Updates Javadocs.

* Close WebClient's response when Jersey's response is closed.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>

---------

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>
  • Loading branch information
spericas committed Sep 20, 2023
1 parent d5168d9 commit 85f5202
Show file tree
Hide file tree
Showing 14 changed files with 431 additions and 81 deletions.
4 changes: 4 additions & 0 deletions jersey/connector/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient-http2</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@

import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Logger;

import javax.net.ssl.SSLContext;

import io.helidon.common.LazyValue;
import io.helidon.common.Version;
import io.helidon.common.tls.Tls;
Expand All @@ -36,10 +35,12 @@
import io.helidon.http.HeaderNames;
import io.helidon.http.Method;
import io.helidon.http.media.ReadableEntity;
import io.helidon.webclient.api.HttpClientRequest;
import io.helidon.webclient.api.HttpClientResponse;
import io.helidon.webclient.api.Proxy;
import io.helidon.webclient.http1.Http1Client;
import io.helidon.webclient.http1.Http1ClientRequest;
import io.helidon.webclient.http1.Http1ClientResponse;
import io.helidon.webclient.api.WebClient;
import io.helidon.webclient.api.WebClientConfig;
import io.helidon.webclient.spi.ProtocolConfig;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.Configuration;
Expand All @@ -50,6 +51,11 @@
import org.glassfish.jersey.client.spi.Connector;
import org.glassfish.jersey.internal.util.PropertiesHelper;

import static io.helidon.jersey.connector.HelidonProperties.DEFAULT_HEADERS;
import static io.helidon.jersey.connector.HelidonProperties.PROTOCOL_CONFIGS;
import static io.helidon.jersey.connector.HelidonProperties.PROTOCOL_ID;
import static io.helidon.jersey.connector.HelidonProperties.SHARE_CONNECTION_CACHE;
import static io.helidon.jersey.connector.HelidonProperties.TLS;
import static org.glassfish.jersey.client.ClientProperties.CONNECT_TIMEOUT;
import static org.glassfish.jersey.client.ClientProperties.FOLLOW_REDIRECTS;
import static org.glassfish.jersey.client.ClientProperties.READ_TIMEOUT;
Expand All @@ -59,6 +65,7 @@ class HelidonConnector implements Connector {
static final Logger LOGGER = Logger.getLogger(HelidonConnector.class.getName());

private static final int DEFAULT_TIMEOUT = 10000;
private static final Map<String, String> EMPTY_MAP_LIST = Map.of("", "");

private static final String HELIDON_VERSION = "Helidon/" + Version.VERSION + " (java "
+ PropertiesHelper.getSystemProperty("java.runtime.version") + ")";
Expand All @@ -67,16 +74,13 @@ class HelidonConnector implements Connector {
LazyValue.create(() -> Executors.newThreadPerTaskExecutor(
Thread.ofVirtual().name("helidon-connector-", 0).factory()));

private final Client client;
private final Http1Client httpClient;
private Proxy proxy;
private final WebClient webClient;
private final Proxy proxy;

HelidonConnector(Client client, Configuration config) {
this.client = client;

// create underlying HTTP client
Map<String, Object> properties = config.getProperties();
var builder = Http1Client.builder();
var builder = WebClientConfig.builder();

// use config for client
builder.config(helidonConfig(config).orElse(Config.empty()));
Expand All @@ -94,22 +98,49 @@ class HelidonConnector implements Connector {
if (properties.containsKey(FOLLOW_REDIRECTS)) {
builder.followRedirects(getValue(properties, FOLLOW_REDIRECTS, true));
}
httpClient = builder.build();

// prefer Tls over SSLContext
if (properties.containsKey(TLS)) {
builder.tls(getValue(properties, TLS, Tls.class));
} else if (client.getSslContext() != null) {
builder.tls(Tls.builder().sslContext(client.getSslContext()).build());
}

// protocol configs
if (properties.containsKey(PROTOCOL_CONFIGS)) {
List<? extends ProtocolConfig> protocolConfigs =
(List<? extends ProtocolConfig>) properties.get(PROTOCOL_CONFIGS);
if (protocolConfigs != null) {
builder.addProtocolConfigs(protocolConfigs);
}
}

// default headers
if (properties.containsKey(DEFAULT_HEADERS)) {
builder.defaultHeadersMap(getValue(properties, DEFAULT_HEADERS, EMPTY_MAP_LIST));
}

// connection sharing defaults to false in this connector
if (properties.containsKey(SHARE_CONNECTION_CACHE)) {
builder.shareConnectionCache(getValue(properties, SHARE_CONNECTION_CACHE, false));
}

webClient = builder.build();
}

/**
* Map a Jersey request to a Helidon HTTP/1.1 request.
* Map a Jersey request to a Helidon HTTP request.
*
* @param request the request to map
* @return the mapped request
*/
private Http1ClientRequest mapRequest(ClientRequest request) {
private HttpClientRequest mapRequest(ClientRequest request) {
// possibly override proxy in request
Proxy requestProxy = ProxyBuilder.createProxy(request).orElse(proxy);

// create WebClient request
URI uri = request.getUri();
Http1ClientRequest httpRequest = httpClient
HttpClientRequest httpRequest = webClient
.method(Method.create(request.getMethod()))
.proxy(requestProxy)
.uri(uri);
Expand All @@ -131,16 +162,18 @@ private Http1ClientRequest mapRequest(ClientRequest request) {
httpRequest.header(HeaderNames.create(key), values);
});

// SSL context
SSLContext sslContext = client.getSslContext();
httpRequest.tls(Tls.builder().sslContext(sslContext).build());

// request config
if (request.hasProperty(FOLLOW_REDIRECTS)) {
httpRequest.followRedirects(request.resolveProperty(FOLLOW_REDIRECTS, true));
Boolean followRedirects = request.resolveProperty(FOLLOW_REDIRECTS, Boolean.class);
if (followRedirects != null) {
httpRequest.followRedirects(followRedirects);
}
Integer readTimeout = request.resolveProperty(READ_TIMEOUT, Integer.class);
if (readTimeout != null) {
httpRequest.readTimeout(Duration.ofMillis(readTimeout));
}
if (request.hasProperty(READ_TIMEOUT)) {
httpRequest.readTimeout(Duration.ofMillis(request.resolveProperty(READ_TIMEOUT, DEFAULT_TIMEOUT)));
String protocolId = request.resolveProperty(PROTOCOL_ID, String.class);
if (protocolId != null) {
httpRequest.protocolId(protocolId);
}

// copy properties
Expand All @@ -167,8 +200,8 @@ private Http1ClientRequest mapRequest(ClientRequest request) {
* @param request the request
* @return the mapped response
*/
private ClientResponse mapResponse(Http1ClientResponse httpResponse, ClientRequest request) {
ClientResponse response = new ClientResponse(new Response.StatusType() {
private ClientResponse mapResponse(HttpClientResponse httpResponse, ClientRequest request) {
Response.StatusType statusType = new Response.StatusType() {
@Override
public int getStatusCode() {
return httpResponse.status().code();
Expand All @@ -183,7 +216,14 @@ public Response.Status.Family getFamily() {
public String getReasonPhrase() {
return httpResponse.status().reasonPhrase();
}
}, request);
};
ClientResponse response = new ClientResponse(statusType, request) {
@Override
public void close() {
super.close();
httpResponse.close(); // closes WebClient's response
}
};

// copy headers
for (Header header : httpResponse.headers()) {
Expand Down Expand Up @@ -211,8 +251,8 @@ public String getReasonPhrase() {
*/
@Override
public ClientResponse apply(ClientRequest request) {
Http1ClientResponse httpResponse;
Http1ClientRequest httpRequest = mapRequest(request);
HttpClientResponse httpResponse;
HttpClientRequest httpRequest = mapRequest(request);

if (request.hasEntity()) {
httpResponse = httpRequest.outputStream(os -> {
Expand Down Expand Up @@ -253,8 +293,8 @@ public String getName() {
public void close() {
}

Http1Client client() {
return httpClient;
WebClient client() {
return webClient;
}

Proxy proxy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,45 @@

package io.helidon.jersey.connector;

import java.io.OutputStream;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.Configuration;
import org.glassfish.jersey.client.spi.Connector;
import org.glassfish.jersey.client.spi.ConnectorProvider;

/**
* Provider for Helidon WebClient {@link Connector} that utilizes the Helidon HTTP Client to send and receive
* HTTP request and responses.
* A Jersey {@link ConnectorProvider} that uses a {@link io.helidon.webclient.api.WebClient}
* instance to executed HTTP requests on behalf of a Jakarta REST {@link Client}.
* <p>
* An instance of this class can be specified during the creation of a {@link Client}
* using the method {@link org.glassfish.jersey.client.ClientConfig#connectorProvider}.
* It is recommended to use the static method {@link #create()} for obtain an
* instance of this class.
* <p>
* Configuration of a connector is driven by properties set on a {@link Client}
* instance, including possibly a config tree. There is a combination of Jersey
* and Helidon properties that can be specified for that purpose. Jersey properties
* are defined in class {@link org.glassfish.jersey.client.ClientProperties} and Helidon
* properties are defined in {@link HelidonProperties}.
* <p>
* The following properties are only supported at construction of this class:
* Only the following properties from {@link org.glassfish.jersey.client.ClientProperties}
* are supported:
* <ul>
* <li>{@link org.glassfish.jersey.client.ClientProperties#CONNECT_TIMEOUT}</li>
* <li>{@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS}</li>
* <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li>
* <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li>
* <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li>
* <li>{@link org.glassfish.jersey.client.ClientProperties#READ_TIMEOUT}</li>
* </ul>
* <p>
* If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an
* entity is not read from the response then
* {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called
* after processing the response to release connection-based resources.
* </p>
* <p>
* Client operations are thread safe, the HTTP connection may
* be shared between different threads.
* </p>
* Client operations are thread safe, the HTTP connection may be shared between
* different threads.
* <p>
* If a response entity is obtained that is an instance of {@link java.io.Closeable}
* then the instance MUST be closed after processing the entity to release
* connection-based resources.
* </p>
* <p>
* This connector uses {@link org.glassfish.jersey.client.ClientProperties#OUTBOUND_CONTENT_LENGTH_BUFFER} to buffer the entity
* written for instance by {@link jakarta.ws.rs.core.StreamingOutput}. Should the buffer be small and
* {@link jakarta.ws.rs.core.StreamingOutput#write(OutputStream)} be called many times, the performance can drop. The Content-Length
* or the Content_Encoding header is set by the underlaying Helidon WebClient regardless of the
* {@link org.glassfish.jersey.client.ClientProperties#OUTBOUND_CONTENT_LENGTH_BUFFER} size, however.
* </p>
*
*/
public class HelidonConnectorProvider implements ConnectorProvider {
/**
Expand All @@ -71,4 +67,13 @@ public HelidonConnectorProvider() {
public Connector getConnector(Client client, Configuration runtimeConfig) {
return new HelidonConnector(client, runtimeConfig);
}

/**
* Create a new instance of {@link HelidonConnectorProvider}.
*
* @return new instance of this class
*/
public static HelidonConnectorProvider create() {
return new HelidonConnectorProvider();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@

package io.helidon.jersey.connector;

import java.util.List;
import java.util.Map;

import io.helidon.common.tls.Tls;
import io.helidon.config.Config;
import io.helidon.webclient.api.WebClient;

/**
* Configuration options specific to the Client API that utilizes {@link HelidonConnector}.
Expand All @@ -27,8 +32,56 @@ private HelidonProperties() {
}

/**
* A Helidon {@link Config} instance used to create the corresponding {@link io.helidon.webclient.api.WebClient}.
* Property name to set a {@link Config} instance to by used by underlying {@link WebClient}.
* This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}.
*
* @see io.helidon.webclient.api.WebClientConfig.Builder#config(io.helidon.common.config.Config)
*/
public static final String CONFIG = "jersey.connector.helidon.config";

/**
* Property name to set a {@link Tls} instance to be used by underlying {@link WebClient}.
* This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}.
*
* @see io.helidon.webclient.api.WebClientConfig.Builder#tls(Tls)
*/
public static final String TLS = "jersey.connector.helidon.tls";

/**
* Property name to set a {@code List<? extends ProtocolConfig>} instance with a list of
* protocol configs to be used by underlying {@link WebClient}.
* This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}.
*
* @see io.helidon.webclient.api.WebClientConfig.Builder#protocolConfigs(List)
*/
public static final String PROTOCOL_CONFIGS = "jersey.connector.helidon.protocolConfigs";

/**
* Property name to set a {@code Map<String, String>} instance with a list of
* default headers to be used by underlying {@link WebClient}.
* This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}.
*
* @see io.helidon.webclient.api.WebClientConfig.Builder#defaultHeadersMap(Map)
*/
public static final String DEFAULT_HEADERS = "jersey.connector.helidon.defaultHeaders";

/**
* Property name to set a protocol ID for each request. You can use this property
* to request an HTTP/2 upgrade from HTTP/1.1 by setting its value to {@code "h2"}.
* When using TLS, Helidon uses negotiation via the ALPN extension instead of this
* property.
*
* @see io.helidon.webclient.api.HttpClientRequest#protocolId(String)
*/
public static final String PROTOCOL_ID = "jersey.connector.helidon.protocolId";

/**
* Property name to enable or disable connection caching in the underlying {@link WebClient}.
* The default for the Helidon connector is {@code false}, or no sharing (which is the
* opposite of {@link WebClient}). Set this property to {@code true} to enable connection
* caching.
*
* @see io.helidon.webclient.api.WebClientConfig.Builder#shareConnectionCache(boolean)
*/
public static final String SHARE_CONNECTION_CACHE = "jersey.connector.helidon.shareConnectionCache";
}
1 change: 1 addition & 0 deletions jersey/connector/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

requires io.helidon.config;
requires io.helidon.webclient;
requires io.helidon.webclient.http2;
requires jakarta.ws.rs;
requires java.logging;
requires jersey.common;
Expand Down
Loading

0 comments on commit 85f5202

Please sign in to comment.