Support Helidon3 and Helidon4 (precedence) by helidon connector. (#5958)
Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/connectors/helidon-connector/pom.xml b/connectors/helidon-connector/pom.xml
index d96442a..734bba0 100644
--- a/connectors/helidon-connector/pom.xml
+++ b/connectors/helidon-connector/pom.xml
@@ -91,7 +91,7 @@
<profile>
<id>HelidonExclude</id>
<activation>
- <jdk>[1.8,17)</jdk>
+ <jdk>[1.8,21)</jdk>
</activation>
<build>
<directory>${java8.build.outputDirectory}</directory>
@@ -129,9 +129,11 @@
<profile>
<id>HelidonInclude</id>
<activation>
- <jdk>[17,)</jdk>
+ <jdk>[21,)</jdk>
</activation>
<build>
+ <!-- 17 is correct, the module works with Helidon 3 supporting JDK 17, too -->
+ <!-- The build is against Helidon 4, which requires JDK 21 for the build, though -->
<directory>${java17.build.outputDirectory}</directory>
<plugins>
<plugin>
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/Helidon3ConnectorProvider.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/Helidon3ConnectorProvider.java
new file mode 100644
index 0000000..2508425
--- /dev/null
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/Helidon3ConnectorProvider.java
@@ -0,0 +1,30 @@
+/*
+ * 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.helidon.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+import org.glassfish.jersey.Beta;
+import org.glassfish.jersey.client.spi.Connector;
+
+@Beta
+class Helidon3ConnectorProvider extends io.helidon.jersey.connector.HelidonConnectorProvider {
+ @Override
+ public Connector getConnector(Client client, Configuration runtimeConfig) {
+ return super.getConnector(client, runtimeConfig);
+ }
+}
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonClientProperties.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonClientProperties.java
index 93baa42..aaf7126 100644
--- a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonClientProperties.java
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonClientProperties.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 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
@@ -13,13 +13,16 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
+
package org.glassfish.jersey.helidon.connector;
-import io.helidon.jersey.connector.HelidonProperties;
+import io.helidon.common.tls.Tls;
+import io.helidon.config.Config;
+import io.helidon.webclient.api.WebClient;
import org.glassfish.jersey.internal.util.PropertiesClass;
-import io.helidon.config.Config;
-import io.helidon.webclient.WebClient;
+import java.util.List;
+import java.util.Map;
/**
* Configuration options specific to the Client API that utilizes {@code HelidonConnectorProvider}.
@@ -28,8 +31,60 @@
@PropertiesClass
public final class HelidonClientProperties {
+ private HelidonClientProperties() {
+ }
+
/**
- * A Helidon {@link Config} instance that is passed to {@link WebClient.Builder#config(Config)} if available
+ * 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 = HelidonProperties.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";
}
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnector.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnector.java
new file mode 100644
index 0000000..c8c5f5b
--- /dev/null
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnector.java
@@ -0,0 +1,343 @@
+/*
+ * 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.helidon.connector;
+
+import io.helidon.common.LazyValue;
+import io.helidon.common.Version;
+import io.helidon.common.tls.Tls;
+import io.helidon.config.Config;
+import io.helidon.http.Header;
+import io.helidon.http.HeaderNames;
+import io.helidon.http.Method;
+import io.helidon.http.media.ReadableEntity;
+import io.helidon.service.registry.ServiceRegistryException;
+import io.helidon.service.registry.Services;
+import io.helidon.webclient.api.HttpClientRequest;
+import io.helidon.webclient.api.HttpClientResponse;
+import io.helidon.webclient.api.Proxy;
+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;
+import jakarta.ws.rs.core.Response;
+
+
+import java.net.URI;
+import java.security.AccessController;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.innate.VirtualThreadUtil;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+
+class HelidonConnector implements Connector {
+ static final System.Logger LOGGER = System.getLogger(HelidonConnector.class.getName());
+ private static final String CONNECTOR_CONFIG_ROOT = "jersey.connector.webclient";
+
+ private static final String HELIDON_VERSION = "Helidon/" + Version.VERSION + " (java "
+ + AccessController.doPrivileged(PropertiesHelper.getSystemProperty("java.runtime.version")) + ")";
+
+ private final LazyValue<ExecutorService> executorService;
+ private final WebClient webClient;
+ private final Proxy proxy;
+
+ @SuppressWarnings("unchecked")
+ HelidonConnector(Client client, Configuration configuration) {
+ executorService = LazyValue.create(() -> VirtualThreadUtil.withConfig(configuration,
+ VirtualThreadUtil.threadFactoryBuilder("helidon-connector-", 0L) ,true).newCachedThreadPool());
+
+ // create underlying HTTP client
+ Map<String, Object> properties = configuration.getProperties();
+ WebClientConfig.Builder builder = WebClientConfig.builder();
+
+ // use config from property first then registry
+ var helidonConfig = configuration.getProperty(HelidonClientProperties.CONFIG);
+ if (helidonConfig != null) {
+ if (helidonConfig instanceof Config) {
+ builder.config((Config) helidonConfig);
+ } else {
+ LOGGER.log(System.Logger.Level.WARNING,
+ LocalizationMessages.HELIDON_CONFIG_IGNORED(HelidonClientProperties.CONFIG));
+ builder.config(configFromRegistry());
+ }
+ } else {
+ builder.config(configFromRegistry());
+ }
+
+ // proxy support
+ proxy = ProxyBuilder.createProxy(configuration).orElse(Proxy.create());
+
+ // possibly override config with properties defined in Jersey client
+ // property values are ignored if cannot be converted to correct type
+ if (properties.containsKey(ClientProperties.CONNECT_TIMEOUT)) {
+ Integer connectTimeout = ClientProperties.getValue(properties, ClientProperties.CONNECT_TIMEOUT, Integer.class);
+ if (connectTimeout != null) {
+ builder.connectTimeout(Duration.ofMillis(connectTimeout));
+ }
+ }
+ if (properties.containsKey(ClientProperties.READ_TIMEOUT)) {
+ Integer readTimeout = ClientProperties.getValue(properties, ClientProperties.READ_TIMEOUT, Integer.class);
+ if (readTimeout != null) {
+ builder.readTimeout(Duration.ofMillis(readTimeout));
+ }
+ }
+ if (properties.containsKey(ClientProperties.FOLLOW_REDIRECTS)) {
+ Boolean followRedirects = ClientProperties.getValue(properties, ClientProperties.FOLLOW_REDIRECTS, Boolean.class);
+ if (followRedirects != null) {
+ builder.followRedirects(followRedirects);
+ }
+ }
+ if (properties.containsKey(ClientProperties.EXPECT_100_CONTINUE)) {
+ Boolean expect100Continue = ClientProperties
+ .getValue(properties, ClientProperties.EXPECT_100_CONTINUE, Boolean.class);
+ if (expect100Continue != null) {
+ builder.sendExpectContinue(expect100Continue);
+ }
+ }
+
+ // first check property and then the Jersey client SSL config
+ boolean isTlsSet = false;
+ if (properties.containsKey(HelidonClientProperties.TLS)) {
+ Tls tls = ClientProperties.getValue(properties, HelidonClientProperties.TLS, Tls.class);
+ if (tls != null) {
+ builder.tls(tls);
+ isTlsSet = true;
+ }
+ }
+ if (!isTlsSet && client.getSslContext() != null) {
+ boolean isJerseyClient = client instanceof JerseyClient;
+ boolean jerseyHasDefaultSsl = isJerseyClient && ((JerseyClient) client).isDefaultSslContext();
+ if (!isJerseyClient || !jerseyHasDefaultSsl) {
+ builder.tls(Tls.builder().sslContext(client.getSslContext()).build());
+ }
+ }
+
+ // protocol configs
+ if (properties.containsKey(HelidonClientProperties.PROTOCOL_CONFIGS)) {
+ List<? extends ProtocolConfig> protocolConfigs =
+ (List<? extends ProtocolConfig>) properties.get(HelidonClientProperties.PROTOCOL_CONFIGS);
+ if (protocolConfigs != null) {
+ builder.addProtocolConfigs(protocolConfigs);
+ }
+ }
+
+ // default headers
+ if (properties.containsKey(HelidonClientProperties.DEFAULT_HEADERS)) {
+ Map<String, String> defaultHeaders = ClientProperties
+ .getValue(properties, HelidonClientProperties.DEFAULT_HEADERS, Map.class);
+ if (defaultHeaders != null) {
+ builder.defaultHeadersMap(defaultHeaders);
+ }
+ }
+
+ // connection sharing defaults to false in this connector
+ if (properties.containsKey(HelidonClientProperties.SHARE_CONNECTION_CACHE)) {
+ Boolean shareConnectionCache = ClientProperties
+ .getValue(properties, HelidonClientProperties.SHARE_CONNECTION_CACHE, Boolean.class);
+ if (shareConnectionCache != null) {
+ builder.shareConnectionCache(shareConnectionCache);
+ }
+ }
+
+ webClient = builder.build();
+ }
+
+ /**
+ * Map a Jersey request to a Helidon HTTP request.
+ *
+ * @param request the request to map
+ * @return the mapped 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();
+ HttpClientRequest httpRequest = webClient
+ .method(Method.create(request.getMethod()))
+ .proxy(requestProxy)
+ .uri(uri);
+
+ // map request headers
+ request.getRequestHeaders().forEach((key, value) -> {
+ String[] values = value.toArray(new String[0]);
+ httpRequest.header(HeaderNames.create(key), values);
+ });
+
+ // request config
+ Boolean followRedirects = request.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, Boolean.class);
+ if (followRedirects != null) {
+ httpRequest.followRedirects(followRedirects);
+ }
+ Integer readTimeout = request.resolveProperty(ClientProperties.READ_TIMEOUT, Integer.class);
+ if (readTimeout != null) {
+ httpRequest.readTimeout(Duration.ofMillis(readTimeout));
+ }
+ String protocolId = request.resolveProperty(HelidonClientProperties.PROTOCOL_ID, String.class);
+ if (protocolId != null) {
+ httpRequest.protocolId(protocolId);
+ }
+
+ // copy properties
+ for (String name : request.getConfiguration().getPropertyNames()) {
+ Object value = request.getConfiguration().getProperty(name);
+ if (!name.startsWith("jersey") && value instanceof String) {
+ httpRequest.property(name, (String) value);
+ }
+ }
+ for (String propertyName : request.getPropertyNames()) {
+ Object value = request.resolveProperty(propertyName, Object.class);
+ if (!propertyName.startsWith("jersey") && value instanceof String) {
+ httpRequest.property(propertyName, (String) value);
+ }
+ }
+
+ return httpRequest;
+ }
+
+ /**
+ * Map a Helidon HTTP/1.1 response to a Jersey response.
+ *
+ * @param httpResponse the response to map
+ * @param request the request
+ * @return the mapped response
+ */
+ private ClientResponse mapResponse(HttpClientResponse httpResponse, ClientRequest request) {
+ Response.StatusType statusType = new Response.StatusType() {
+ @Override
+ public int getStatusCode() {
+ return httpResponse.status().code();
+ }
+
+ @Override
+ public Response.Status.Family getFamily() {
+ return Response.Status.Family.familyOf(getStatusCode());
+ }
+
+ @Override
+ public String getReasonPhrase() {
+ return httpResponse.status().reasonPhrase();
+ }
+ };
+ 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()) {
+ for (String v : header.allValues()) {
+ response.getHeaders().add(header.name(), v);
+ }
+ }
+
+ // last URI, possibly after redirections
+ response.setResolvedRequestUri(httpResponse.lastEndpointUri().toUri());
+
+ // handle entity
+ ReadableEntity entity = httpResponse.entity();
+ if (entity.hasEntity()) {
+ response.setEntityStream(entity.inputStream());
+ }
+ return response;
+ }
+
+ /**
+ * Execute Jersey request using WebClient.
+ *
+ * @param request the Jersey request
+ * @return a Jersey response
+ */
+ @Override
+ public ClientResponse apply(ClientRequest request) {
+ HttpClientResponse httpResponse;
+ HttpClientRequest httpRequest = mapRequest(request);
+
+ if (request.hasEntity()) {
+ httpResponse = httpRequest.outputStream(os -> {
+ request.setStreamProvider(length -> os);
+ request.writeEntity();
+ });
+ } else {
+ httpResponse = httpRequest.request();
+ }
+
+ return mapResponse(httpResponse, request);
+ }
+
+ /**
+ * Asynchronously execute Jersey request using WebClient.
+ *
+ * @param request the Jersey request
+ * @return a Jersey response
+ */
+ @Override
+ public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) {
+ return executorService.get().submit(() -> {
+ try {
+ ClientResponse response = apply(request);
+ callback.response(response);
+ } catch (Throwable t) {
+ callback.failure(t);
+ }
+ });
+ }
+
+ @Override
+ public String getName() {
+ return HELIDON_VERSION;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ WebClient client() {
+ return webClient;
+ }
+
+ Proxy proxy() {
+ return proxy;
+ }
+
+ Config configFromRegistry() {
+ try {
+ io.helidon.common.config.Config cfg = Services.get(io.helidon.common.config.Config.class);
+ if (cfg instanceof Config) {
+ return ((Config) cfg).get(CONNECTOR_CONFIG_ROOT);
+ }
+ } catch (ServiceRegistryException e) {
+ // falls through
+ }
+ LOGGER.log(System.Logger.Level.TRACE, LocalizationMessages.NO_CONFIG_IN_REGISTERY());
+ return Config.empty();
+ }
+}
\ No newline at end of file
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
index aa1540a..dea881e 100644
--- a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 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,15 +16,20 @@
package org.glassfish.jersey.helidon.connector;
-import org.glassfish.jersey.Beta;
-import org.glassfish.jersey.client.spi.Connector;
-import org.glassfish.jersey.internal.util.JdkVersion;
-
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.Configuration;
+
import java.io.OutputStream;
+import org.glassfish.jersey.Beta;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.internal.util.collection.LazyValue;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
+
/**
* Provider for Helidon WebClient {@link Connector} that utilizes the Helidon HTTP Client to send and receive
* HTTP request and responses. JDK 8 is not supported by the Helidon Connector.
@@ -65,12 +70,20 @@
* @since 2.31
*/
@Beta
-public class HelidonConnectorProvider extends io.helidon.jersey.connector.HelidonConnectorProvider {
+public class HelidonConnectorProvider implements ConnectorProvider {
+ private static final LazyValue<Helidon3ConnectorProvider> helidon3ConnectorProvider =
+ Values.lazy((Value<Helidon3ConnectorProvider>) Helidon3ConnectorProvider::new);
+
+ public HelidonConnectorProvider() {
+ }
+
@Override
public Connector getConnector(Client client, Configuration runtimeConfig) {
- if (JdkVersion.getJdkVersion().getMajor() < 17) {
- throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ if (HelidonVersionChecker.VERSION.get() == HelidonVersionChecker.Version.VERSION_3) {
+ return helidon3ConnectorProvider.get().getConnector(client, runtimeConfig);
+ } else if (JdkVersion.getJdkVersion().getMajor() < 21) {
+ throw new ProcessingException(LocalizationMessages.HELIDON_4_NOT_SUPPORTED());
}
- return super.getConnector(client, runtimeConfig);
+ return new HelidonConnector(client, runtimeConfig);
}
}
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonVersionChecker.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonVersionChecker.java
new file mode 100644
index 0000000..179c2fa
--- /dev/null
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonVersionChecker.java
@@ -0,0 +1,43 @@
+/*
+ * 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.helidon.connector;
+
+import org.glassfish.jersey.internal.util.collection.LazyValue;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
+
+import java.lang.reflect.Field;
+
+/* package */ class HelidonVersionChecker {
+ static enum Version {
+ VERSION_3,
+ VERSION_4;
+ }
+
+ /* package */ static final LazyValue<Version> VERSION = Values.lazy((Value<Version>) () -> {
+ try {
+ // Cannot use io.helidon.common.Version.VERSION as that constant is linked into the code, resulting in 4.2.4 constant
+ // Must do it in the Runtime
+ Field field = Class.forName("io.helidon.common.Version").getDeclaredField("VERSION");
+ String version = (String) field.get(null);
+ return version.startsWith("4") ? Version.VERSION_4 : Version.VERSION_3;
+ } catch (Throwable t) {
+ // not helidon 4
+ }
+ return Version.VERSION_3;
+ });
+}
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/ProxyBuilder.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/ProxyBuilder.java
new file mode 100644
index 0000000..819d283
--- /dev/null
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/ProxyBuilder.java
@@ -0,0 +1,86 @@
+/*
+ * 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.helidon.connector;
+
+import java.net.URI;
+import java.util.Locale;
+import java.util.Optional;
+
+import io.helidon.webclient.api.Proxy;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Configuration;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+
+class ProxyBuilder {
+
+ static Optional<Proxy> createProxy(Configuration config) {
+ Object proxyUri = config.getProperty(ClientProperties.PROXY_URI);
+ String userName = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_USERNAME, String.class);
+ String password = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class);
+ return createProxy(proxyUri, userName, password);
+ }
+
+ static Optional<Proxy> createProxy(ClientRequest clientRequest) {
+ Object proxyUri = clientRequest.resolveProperty(ClientProperties.PROXY_URI, Object.class);
+ String userName = clientRequest.resolveProperty(ClientProperties.PROXY_USERNAME, String.class);
+ String password = clientRequest.resolveProperty(ClientProperties.PROXY_PASSWORD, String.class);
+ return createProxy(proxyUri, userName, password);
+ }
+
+ private ProxyBuilder() {
+ }
+
+ private static Optional<Proxy> createProxy(Object proxyUri, String userName, String password) {
+ if (proxyUri != null) {
+ URI u = getProxyUri(proxyUri);
+ Proxy.Builder builder = Proxy.builder();
+ if (u.getScheme().toUpperCase(Locale.ROOT).equals("DIRECT")) {
+ builder.type(Proxy.ProxyType.NONE);
+ } else {
+ builder.host(u.getHost()).port(u.getPort());
+ if ("HTTP".equals(u.getScheme().toUpperCase(Locale.ROOT))) {
+ builder.type(Proxy.ProxyType.HTTP);
+ } else {
+ HelidonConnector.LOGGER.log(System.Logger.Level.WARNING,
+ LocalizationMessages.PROXY_SCHEMA_NOT_SUPPORTED(u.getScheme()));
+ return Optional.empty();
+ }
+ }
+ if (userName != null) {
+ builder.username(userName);
+ if (password != null) {
+ builder.password(password.toCharArray());
+ }
+ }
+ return Optional.of(builder.build());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private static URI getProxyUri(Object proxy) {
+ if (proxy instanceof URI) {
+ return (URI) proxy;
+ } else if (proxy instanceof String) {
+ return URI.create((String) proxy);
+ } else {
+ throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(proxy));
+ }
+ }
+}
diff --git a/connectors/helidon-connector/src/main/java8/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java b/connectors/helidon-connector/src/main/java8/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
index 932155a..f63ef05 100644
--- a/connectors/helidon-connector/src/main/java8/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
+++ b/connectors/helidon-connector/src/main/java8/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.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
@@ -23,7 +23,6 @@
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.Configuration;
-import java.io.OutputStream;
/**
* Helidon Connector stub which only throws exception when running on JDK prior to 17.
@@ -35,7 +34,7 @@
@Override
public Connector getConnector(Client client, Configuration runtimeConfig) {
if (JdkVersion.getJdkVersion().getMajor() < 17) {
- throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ throw new ProcessingException(LocalizationMessages.HELIDON_3_NOT_SUPPORTED());
}
return null;
}
diff --git a/connectors/helidon-connector/src/main/resources/org/glassfish/jersey/helidon/connector/localization.properties b/connectors/helidon-connector/src/main/resources/org/glassfish/jersey/helidon/connector/localization.properties
index fb56d5c..f4e68d2 100644
--- a/connectors/helidon-connector/src/main/resources/org/glassfish/jersey/helidon/connector/localization.properties
+++ b/connectors/helidon-connector/src/main/resources/org/glassfish/jersey/helidon/connector/localization.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2020, 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
@@ -14,4 +14,9 @@
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
#
-not.supported=Helidon connector is not supported on JDK version less than 17.
\ No newline at end of file
+helidon.config.ignored=Ignoring Helidon Connector config at "{0}".
+helidon3.not.supported=Helidon connector is not supported on JDK version less than 17.
+helidon4.not.supported=Helidon 4 connector is not supported on JDK version less than 21.
+no.config.in.registery=Unable to find Config in service registry.
+proxy.schema.not.supported=Proxy schema {0} not supported.
+wrong.proxy.uri.type=The proxy URI "{0}" MUST be String or URI.
\ No newline at end of file
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java b/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java
index 3511d8e..5c7dbdb 100644
--- a/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.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
@@ -18,6 +18,7 @@
import org.glassfish.jersey.CommonProperties;
import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+import org.glassfish.jersey.innate.virtual.ThreadFactoryBuilder;
import jakarta.ws.rs.core.Configuration;
import java.util.concurrent.ThreadFactory;
@@ -26,6 +27,9 @@
* Factory class to provide JDK specific implementation of bits related to the virtual thread support.
*/
public final class VirtualThreadUtil {
+ public static ThreadFactoryBuilder threadFactoryBuilder(String prefix, long start) {
+ return new ThreadFactoryBuilder().prefix(prefix).start(start);
+ }
private static final boolean USE_VIRTUAL_THREADS_BY_DEFAULT = false;
@@ -48,10 +52,23 @@
/**
* Return an instance of {@link LoomishExecutors} based on a configuration property.
* @param config the {@link Configuration}
- * @param useVirtualByDefault the default use if not said otherwise by property
+ * @param useVirtualByDefault the default use if not said otherwise by property.
* @return the {@link LoomishExecutors} instance.
*/
public static LoomishExecutors withConfig(Configuration config, boolean useVirtualByDefault) {
+ return withConfig(config, null, useVirtualByDefault);
+ }
+
+ /**
+ * Return an instance of {@link LoomishExecutors} based on a configuration property.
+ * @param config the {@link Configuration}
+ * @param threadFactoryBuilder the information for the thread factory.
+ * @param useVirtualByDefault the default use if not said otherwise by property.
+ * @return the {@link LoomishExecutors} instance.
+ */
+ public static LoomishExecutors withConfig(Configuration config,
+ ThreadFactoryBuilder threadFactoryBuilder,
+ boolean useVirtualByDefault) {
ThreadFactory tfThreadFactory = null;
boolean useVirtualThreads = useVirtualThreads(config, useVirtualByDefault);
@@ -59,6 +76,8 @@
Object threadFactory = config.getProperty(CommonProperties.THREAD_FACTORY);
if (threadFactory != null && ThreadFactory.class.isInstance(threadFactory)) {
tfThreadFactory = (ThreadFactory) threadFactory;
+ } else if (threadFactoryBuilder != null) {
+ return VirtualThreadSupport.allowVirtual(useVirtualThreads, threadFactoryBuilder);
}
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/virtual/ThreadFactoryBuilder.java b/core-common/src/main/java/org/glassfish/jersey/innate/virtual/ThreadFactoryBuilder.java
new file mode 100644
index 0000000..383015c
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/virtual/ThreadFactoryBuilder.java
@@ -0,0 +1,62 @@
+/*
+ * 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.innate.virtual;
+
+/**
+ * Bearer of the information used for building {@code ThreadFactory} either by using virtual threads or platform threads.
+ */
+public class ThreadFactoryBuilder {
+ private long start = 0L;
+ private String prefix = null;
+
+ /**
+ * Get the thread name prefix.
+ * @return the thread name prefix.
+ */
+ public String prefix() {
+ return prefix;
+ }
+
+ /**
+ * The prefix of the name of the thread. For instance "my-factory-thread-".
+ * @param prefix the thread name prefix.
+ * @return
+ */
+ public ThreadFactoryBuilder prefix(String prefix) {
+ this.prefix = prefix;
+ return this;
+ }
+
+ /**
+ * Get the initial id for the first thread created by the thread factory.
+ * @return the initial thread id.
+ */
+ public long start() {
+ return start;
+ }
+
+ /**
+ * Set the initial id for the first thread created by the thread factory. The prefix and the id make the name of the thread.
+ * For instance "my-factory-thread-0".
+ * @param start the initial id for the first thread created by the thread factory.
+ * @return the updated builder.
+ */
+ public ThreadFactoryBuilder start(long start) {
+ this.start = start;
+ return this;
+ }
+}
diff --git a/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java b/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java
index 867a65b..12e06d3 100644
--- a/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java
+++ b/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.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
@@ -17,6 +17,7 @@
package org.glassfish.jersey.innate;
import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+import org.glassfish.jersey.innate.virtual.ThreadFactoryBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -56,13 +57,30 @@
/**
* Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
* @param allow whether to allow virtual threads.
- * @param threadFactory the thread factory to be used by a the {@link ExecutorService}.
+ * @param threadFactory the thread factory to be used by the {@link ExecutorService}.
* @return the {@link LoomishExecutors} instance.
*/
public static LoomishExecutors allowVirtual(boolean allow, ThreadFactory threadFactory) {
return new NonLoomishExecutors(threadFactory);
}
+ /**
+ * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
+ * @param allow whether to allow virtual threads.
+ * @param threadFactoryBuilder the builder used to build thread factory to be used by the {@link ExecutorService}.
+ * @return the {@link LoomishExecutors} instance.
+ */
+ public static LoomishExecutors allowVirtual(boolean allow, ThreadFactoryBuilder threadFactoryBuilder) {
+ ThreadFactory threadFactory = new ThreadFactory() {
+ private long index = threadFactoryBuilder.start();
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, threadFactoryBuilder.prefix() + index++);
+ }
+ };
+ return new NonLoomishExecutors(threadFactory);
+ }
+
private static final class NonLoomishExecutors implements LoomishExecutors {
private final ThreadFactory threadFactory;
diff --git a/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java b/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java
index 0e7d695..14bc024 100644
--- a/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java
+++ b/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.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
@@ -17,6 +17,7 @@
package org.glassfish.jersey.innate;
import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+import org.glassfish.jersey.innate.virtual.ThreadFactoryBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -57,13 +58,33 @@
/**
* Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
* @param allow whether to allow virtual threads.
- * @param threadFactory the thread factory to be used by a the {@link ExecutorService}.
+ * @param threadFactory the thread factory to be used by the {@link ExecutorService}.
* @return the {@link LoomishExecutors} instance.
*/
public static LoomishExecutors allowVirtual(boolean allow, ThreadFactory threadFactory) {
return allow ? new Java21LoomishExecutors(threadFactory) : new NonLoomishExecutors(threadFactory);
}
+ /**
+ * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
+ * @param allow whether to allow virtual threads.
+ * @param threadFactoryBuilder the builder used to build thread factory to be used by the {@link ExecutorService}.
+ * @return the {@link LoomishExecutors} instance.
+ */
+ public static LoomishExecutors allowVirtual(boolean allow, ThreadFactoryBuilder threadFactoryBuilder) {
+ return allow ? loomish(threadFactoryBuilder) : nonLoomish(threadFactoryBuilder);
+ }
+
+ private static LoomishExecutors nonLoomish(ThreadFactoryBuilder threadFactoryBuilder) {
+ return new NonLoomishExecutors(
+ Thread.ofPlatform().name(threadFactoryBuilder.prefix(), threadFactoryBuilder.start()).factory());
+ }
+
+ private static LoomishExecutors loomish(ThreadFactoryBuilder threadFactoryBuilder) {
+ return new Java21LoomishExecutors(
+ Thread.ofVirtual().name(threadFactoryBuilder.prefix(), threadFactoryBuilder.start()).factory());
+ }
+
private static class NonLoomishExecutors implements LoomishExecutors {
private final ThreadFactory threadFactory;
diff --git a/pom.xml b/pom.xml
index 772798d..305114e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2192,7 +2192,8 @@
<microprofile.rest.client.version>4.0</microprofile.rest.client.version>
<helidon.config.version>3.2.12</helidon.config.version>
<helidon.container.version>4.2.4</helidon.container.version>
- <helidon.connector.version>3.2.12</helidon.connector.version>
+ <helidon3.connector.version>3.2.12</helidon3.connector.version>
+ <helidon.connector.version>4.2.4</helidon.connector.version>
<helidon.config.11.version>1.4.15</helidon.config.11.version> <!-- JDK 11- support -->
<smallrye.config.version>3.7.1</smallrye.config.version>
diff --git a/tests/integration/helidon3-client/pom.xml b/tests/integration/helidon3-client/pom.xml
new file mode 100644
index 0000000..04fba08
--- /dev/null
+++ b/tests/integration/helidon3-client/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>project</artifactId>
+ <groupId>org.glassfish.jersey.tests.integration</groupId>
+ <version>3.1.99-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>helidon3-client</artifactId>
+ <name>jersey-helidon-client3-integration</name>
+ <description>
+ Checks if Helidon 3 works with Helidon connector.
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.glassfish.jersey.connectors</groupId>
+ <artifactId>jersey-helidon-connector</artifactId>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>io.helidon.jersey</groupId>
+ <artifactId>helidon-jersey-connector</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>io.helidon.jersey</groupId>
+ <artifactId>helidon-jersey-connector</artifactId>
+ <version>${helidon3.connector.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+ <artifactId>jersey-test-framework-provider-external</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+ <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework</groupId>
+ <artifactId>jersey-test-framework-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.surefire</groupId>
+ <artifactId>surefire</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/tests/integration/helidon3-client/src/test/java/org/glassfish/jersey/integration/helidon/Helidon3Test.java b/tests/integration/helidon3-client/src/test/java/org/glassfish/jersey/integration/helidon/Helidon3Test.java
new file mode 100644
index 0000000..78a2405
--- /dev/null
+++ b/tests/integration/helidon3-client/src/test/java/org/glassfish/jersey/integration/helidon/Helidon3Test.java
@@ -0,0 +1,67 @@
+/*
+ * 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.integration.helidon;
+
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.helidon.connector.HelidonConnectorProvider;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class Helidon3Test extends JerseyTest {
+
+ @Path("/")
+ public static class Helidon3TestResource {
+ @POST
+ @Path("version")
+ public String header(@Context HttpHeaders headers, String content) {
+ return headers.getHeaderString(HttpHeaders.USER_AGENT);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(Helidon3TestResource.class);
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new HelidonConnectorProvider());
+ super.configureClient(config);
+ }
+
+ @Test
+ public void testPostWithHelidon3() {
+ System.out.println("Helidon Version " + io.helidon.common.Version.VERSION);
+ Assertions.assertEquals('3', io.helidon.common.Version.VERSION.charAt(0));
+ try (Response response = target("version").request().post(Entity.entity("ANYTHING", MediaType.TEXT_PLAIN_TYPE))) {
+ Assertions.assertEquals(200, response.getStatus());
+ Assertions.assertTrue(response.readEntity(String.class).contains("Helidon/3"));
+ }
+ }
+}
diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml
index b69188a..49fc438 100644
--- a/tests/integration/pom.xml
+++ b/tests/integration/pom.xml
@@ -128,6 +128,7 @@
<modules>
<module>async-jersey-filter</module>
<module>externalproperties</module>
+ <module>helidon3-client</module>
<module>jaxrs-component-inject</module>
<module>jersey-780</module>
<module>jersey-1107</module>