From 51671a5d58a1a0197cec2e4522b004145f7c9960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Nie=C3=9Fing?= Date: Sat, 2 Sep 2023 17:43:21 +0200 Subject: [PATCH] Add Jetty 11.x client connector --- bom/pom.xml | 5 + connectors/jetty11-connector/pom.xml | 140 +++++ .../connector/Jetty11ClientProperties.java | 123 +++++ .../jetty11/connector/Jetty11Connector.java | 522 ++++++++++++++++++ .../connector/Jetty11ConnectorProvider.java | 128 +++++ .../connector/Jetty11HttpClientContract.java | 33 ++ .../connector/Jetty11HttpClientSupplier.java | 56 ++ .../jetty11/connector/package-info.java | 21 + .../jetty11/connector/localization.properties | 21 + .../jersey/jetty11/connector/AsyncTest.java | 201 +++++++ .../jetty11/connector/AuthFilterTest.java | 77 +++ .../jersey/jetty11/connector/AuthTest.java | 197 +++++++ .../jersey/jetty11/connector/CookieTest.java | 121 ++++ .../connector/CustomLoggingFilter.java | 70 +++ .../jersey/jetty11/connector/EntityTest.java | 158 ++++++ .../jersey/jetty11/connector/ErrorTest.java | 124 +++++ .../connector/FollowRedirectsTest.java | 142 +++++ .../connector/GZIPContentEncodingTest.java | 105 ++++ .../jetty11/connector/HelloWorldTest.java | 225 ++++++++ .../jetty11/connector/HttpHeadersTest.java | 101 ++++ .../jetty11/connector/ManagedClientTest.java | 256 +++++++++ .../jersey/jetty11/connector/MethodTest.java | 153 +++++ .../jetty11/connector/NoEntityTest.java | 102 ++++ .../connector/SyncResponseSizeTest.java | 171 ++++++ .../jersey/jetty11/connector/TimeoutTest.java | 245 ++++++++ .../jetty11/connector/TraceSupportTest.java | 235 ++++++++ .../UnderlyingHttpClientAccessTest.java | 72 +++ connectors/pom.xml | 1 + docs/src/main/docbook/appendix-properties.xml | 3 +- docs/src/main/docbook/client.xml | 28 +- docs/src/main/docbook/dependencies.xml | 9 +- docs/src/main/docbook/jersey.ent | 9 + docs/src/main/docbook/modules.xml | 12 +- 33 files changed, 3857 insertions(+), 9 deletions(-) create mode 100644 connectors/jetty11-connector/pom.xml create mode 100644 connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11ClientProperties.java create mode 100644 connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java create mode 100644 connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11ConnectorProvider.java create mode 100644 connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11HttpClientContract.java create mode 100644 connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11HttpClientSupplier.java create mode 100644 connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/package-info.java create mode 100644 connectors/jetty11-connector/src/main/resources/org/glassfish/jersey/jetty11/connector/localization.properties create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/AsyncTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/AuthFilterTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/AuthTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/CookieTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/CustomLoggingFilter.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/EntityTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/ErrorTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/FollowRedirectsTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/GZIPContentEncodingTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/HelloWorldTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/HttpHeadersTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/ManagedClientTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/MethodTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/NoEntityTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/SyncResponseSizeTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/TimeoutTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/TraceSupportTest.java create mode 100644 connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/UnderlyingHttpClientAccessTest.java diff --git a/bom/pom.xml b/bom/pom.xml index feb6cba09a..1bcf00e289 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -88,6 +88,11 @@ jersey-jetty-connector ${project.version} + + org.glassfish.jersey.connectors + jersey-jetty11-connector + ${project.version} + org.glassfish.jersey.connectors jersey-jdk-connector diff --git a/connectors/jetty11-connector/pom.xml b/connectors/jetty11-connector/pom.xml new file mode 100644 index 0000000000..32b76c4cd9 --- /dev/null +++ b/connectors/jetty11-connector/pom.xml @@ -0,0 +1,140 @@ + + + + + 4.0.0 + + + org.glassfish.jersey.connectors + project + 3.1.99-SNAPSHOT + + + jersey-jetty11-connector + jar + jersey-connectors-jetty11 + + Jersey Client Transport via Jetty 11.x + + + UTF-8 + + + + + org.eclipse.jetty + jetty-client + ${jetty11.version} + + + org.slf4j + slf4j-api + + + org.eclipse.jetty + jetty-util + + + + + org.eclipse.jetty + jetty-util + ${jetty11.version} + + + org.slf4j + slf4j-api + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + test + + + org.glassfish.jersey.media + jersey-media-jaxb + ${project.version} + test + + + org.glassfish.jersey.containers + jersey-container-grizzly2-http + ${project.version} + test + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${project.version} + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${project.version} + test + + + jakarta.xml.bind + jakarta.xml.bind-api + 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-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11ClientProperties.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11ClientProperties.java new file mode 100644 index 0000000000..d2d9986175 --- /dev/null +++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11ClientProperties.java @@ -0,0 +1,123 @@ +/* + * 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.jetty11.connector; + +import java.util.Map; + +import org.glassfish.jersey.internal.util.PropertiesClass; +import org.glassfish.jersey.internal.util.PropertiesHelper; + +/** + * Configuration options specific to the Client API that utilizes {@link Jetty11ConnectorProvider}. + * + * @author Arul Dhesiaseelan (aruld at acm.org) + */ +@PropertiesClass +public final class Jetty11ClientProperties { + + /** + * Prevents instantiation. + */ + private Jetty11ClientProperties() { + throw new AssertionError("No instances allowed."); + } + + /** + * A value of {@code false} indicates the client should handle cookies + * automatically using HttpClient's default cookie policy. A value + * of {@code false} will cause the client to ignore all cookies. + *

+ * The value MUST be an instance of {@link Boolean}. + * If the property is absent the default value is {@code false} + */ + public static final String DISABLE_COOKIES = + "jersey.config.jetty11.client.disableCookies"; + + /** + * The credential provider that should be used to retrieve + * credentials from a user. + * + * If an {@link org.eclipse.jetty.client.api.Authentication} mechanism is found, + * it is then used for the given request, returning an {@link org.eclipse.jetty.client.api.Authentication.Result}, + * which is then stored in the {@link org.eclipse.jetty.client.api.AuthenticationStore} + * so that subsequent requests can be preemptively authenticated. + + *

+ * The value MUST be an instance of {@link + * org.eclipse.jetty.client.util.BasicAuthentication}. If + * the property is absent a default provider will be used. + */ + public static final String PREEMPTIVE_BASIC_AUTHENTICATION = + "jersey.config.jetty11.client.preemptiveBasicAuthentication"; + + /** + * A value of {@code false} indicates the client disable a hostname verification + * during SSL Handshake. A client will ignore CN value defined in a certificate + * that is stored in a truststore. + *

+ * The value MUST be an instance of {@link Boolean}. + * If the property is absent the default value is {@code true}. + */ + public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = + "jersey.config.jetty11.client.enableSslHostnameVerification"; + + /** + * Overrides the default Jetty synchronous listener response max buffer size. + * In practise, this allows you to read larger responses. + * Size in bytes. + *

+ * If the property is absent, the value is such as specified by Jetty (currently 2MiB). + */ + public static final String SYNC_LISTENER_RESPONSE_MAX_SIZE = + "jersey.config.jetty11.client.syncListenerResponseMaxSize"; + + /** + * Total timeout interval for request/response conversation, in milliseconds. + * Opposed to {@link org.glassfish.jersey.client.ClientProperties#READ_TIMEOUT}. + *

+ * The value MUST be an instance convertible to {@link Integer}. The + * value of zero (0) is equivalent to an interval of infinity. + *

+ *

+ * The default value is zero (infinity). + *

+ *

+ * The name of the configuration property is {@value}. + *

+ * + * @since 2.37 + */ + public static final String TOTAL_TIMEOUT = "jersey.config.jetty11.client.totalTimeout"; + + /** + * Get the value of the specified property. + * + * If the property is not set or the real value type is not compatible with the specified value type, returns {@code null}. + * + * @param properties Map of properties to get the property value from. + * @param key Name of the property. + * @param type Type to retrieve the value as. + * @param Type of the property value. + * @return Value of the property or {@code null}. + * + * @since 2.8 + */ + public static T getValue(final Map properties, final String key, final Class type) { + return PropertiesHelper.getValue(properties, key, type, null); + } + +} diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java new file mode 100644 index 0000000000..24f10fcc6c --- /dev/null +++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java @@ -0,0 +1,522 @@ +/* + * 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.jetty11.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.MultivaluedMap; + +import javax.net.ssl.SSLContext; + +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpRequest; +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.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; +import org.glassfish.jersey.internal.util.collection.NonBlockingInputStream; +import org.glassfish.jersey.message.internal.HeaderUtils; +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; + +/** + * A {@link Connector} that utilizes the Jetty HTTP Client to send and receive + * HTTP request and responses. + *

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

    + *
  • {@link ClientProperties#ASYNC_THREADPOOL_SIZE}
  • + *
  • {@link ClientProperties#CONNECT_TIMEOUT}
  • + *
  • {@link ClientProperties#FOLLOW_REDIRECTS}
  • + *
  • {@link ClientProperties#PROXY_URI}
  • + *
  • {@link ClientProperties#PROXY_USERNAME}
  • + *
  • {@link ClientProperties#PROXY_PASSWORD}
  • + *
  • {@link ClientProperties#PROXY_PASSWORD}
  • + *
  • {@link Jetty11ClientProperties#DISABLE_COOKIES}
  • * + *
  • {@link Jetty11ClientProperties#ENABLE_SSL_HOSTNAME_VERIFICATION}
  • + *
  • {@link Jetty11ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}
  • + *
  • {@link Jetty11ClientProperties#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();
+ * Connector connector = new JettyConnector(config);
+ * config.connector(connector);
+ * 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();
+ * }
+ * 
+ *

+ * This connector supports only {@link org.glassfish.jersey.client.RequestEntityProcessing#BUFFERED entity buffering}. + * Defining the property {@link ClientProperties#REQUEST_ENTITY_PROCESSING} has no effect on this connector. + *

+ * + * @author Arul Dhesiaseelan (aruld at acm.org) + * @author Marek Potociar + */ +class Jetty11Connector implements Connector { + + private static final Logger LOGGER = Logger.getLogger(Jetty11Connector.class.getName()); + + private final HttpClient client; + private final CookieStore cookieStore; + private final Configuration configuration; + private final Optional syncListenerResponseMaxSize; + + /** + * Create the new Jetty client connector. + * + * @param jaxrsClient JAX-RS client instance, for which the connector is created. + * @param config client configuration. + */ + Jetty11Connector(final Client jaxrsClient, final Configuration config) { + this.configuration = config; + HttpClient httpClient = null; + if (config.isRegistered(Jetty11HttpClientSupplier.class)) { + Optional contract = config.getInstances().stream() + .filter(a-> Jetty11HttpClientSupplier.class.isInstance(a)).findFirst(); + if (contract.isPresent()) { + httpClient = ((Jetty11HttpClientSupplier) contract.get()).getHttpClient(); + } + } + if (httpClient == null) { + final SSLContext sslContext = jaxrsClient.getSslContext(); + 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); + httpClient = new HttpClient(transport); + } + this.client = httpClient; + + Boolean enableHostnameVerification = (Boolean) config.getProperties() + .get(Jetty11ClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION); + 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); + if (connectTimeout != null && connectTimeout instanceof Integer && (Integer) connectTimeout > 0) { + client.setConnectTimeout((Integer) connectTimeout); + } + final Object threadPoolSize = config.getProperties().get(ClientProperties.ASYNC_THREADPOOL_SIZE); + if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) { + final String name = HttpClient.class.getSimpleName() + "@" + hashCode(); + final QueuedThreadPool threadPool = new QueuedThreadPool((Integer) threadPoolSize); + threadPool.setName(name); + client.setExecutor(threadPool); + } + Boolean disableCookies = (Boolean) config.getProperties().get(Jetty11ClientProperties.DISABLE_COOKIES); + disableCookies = (disableCookies != null) ? disableCookies : false; + + final AuthenticationStore auth = client.getAuthenticationStore(); + final Object basicAuthProvider = config.getProperty(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION); + if (basicAuthProvider != null && (basicAuthProvider instanceof BasicAuthentication)) { + auth.addAuthentication((BasicAuthentication) basicAuthProvider); + } + + final Optional proxy = ClientProxy.proxyFromConfiguration(config); + proxy.ifPresent(clientProxy -> { + final ProxyConfiguration proxyConfig = client.getProxyConfiguration(); + final URI u = clientProxy.uri(); + proxyConfig.getProxies().add(new HttpProxy(u.getHost(), u.getPort())); + + if (clientProxy.userName() != null) { + auth.addAuthentication(new BasicAuthentication(u, "<>", + clientProxy.userName(), clientProxy.password())); + } + }); + + if (disableCookies) { + client.setCookieStore(new HttpCookieStore.Empty()); + } + + final Object slResponseMaxSize = configuration.getProperties() + .get(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE); + if (slResponseMaxSize != null && slResponseMaxSize instanceof Integer + && (Integer) slResponseMaxSize > 0) { + this.syncListenerResponseMaxSize = Optional.of((Integer) slResponseMaxSize); + } + else { + this.syncListenerResponseMaxSize = Optional.empty(); + } + + try { + client.start(); + } catch (final Exception e) { + throw new ProcessingException("Failed to start the client.", e); + } + this.cookieStore = client.getCookieStore(); + } + + /** + * Get the {@link HttpClient}. + * + * @return the {@link HttpClient}. + */ + @SuppressWarnings("UnusedDeclaration") + public HttpClient getHttpClient() { + return client; + } + + /** + * Get the {@link CookieStore}. + * + * @return the {@link CookieStore} instance or null when + * JettyClientProperties.DISABLE_COOKIES set to true. + */ + public CookieStore getCookieStore() { + return cookieStore; + } + + @Override + public ClientResponse apply(final ClientRequest jerseyRequest) throws ProcessingException { + final Request jettyRequest = translateRequest(jerseyRequest); + final Map clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest); + final ContentProvider entity = getBytesProvider(jerseyRequest); + if (entity != null) { + jettyRequest.content(entity); + } + + try { + final ContentResponse jettyResponse; + if (!syncListenerResponseMaxSize.isPresent()) { + jettyResponse = jettyRequest.send(); + } + else { + final FutureResponseListener listener + = new FutureResponseListener(jettyRequest, syncListenerResponseMaxSize.get()); + jettyRequest.send(listener); + jettyResponse = listener.get(); + } + HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(), + Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration()); + + final jakarta.ws.rs.core.Response.StatusType status = jettyResponse.getReason() == null + ? Statuses.from(jettyResponse.getStatus()) + : Statuses.from(jettyResponse.getStatus(), jettyResponse.getReason()); + + final ClientResponse jerseyResponse = new ClientResponse(status, jerseyRequest); + processResponseHeaders(jettyResponse.getHeaders(), jerseyResponse); + try { + jerseyResponse.setEntityStream(new HttpClientResponseInputStream(jettyResponse)); + } catch (final IOException e) { + LOGGER.log(Level.SEVERE, null, e); + } + + return jerseyResponse; + } catch (final Exception e) { + throw new ProcessingException(e); + } + } + + private static void processResponseHeaders(final HttpFields respHeaders, final ClientResponse jerseyResponse) { + for (final HttpField header : respHeaders) { + final String headerName = header.getName(); + final MultivaluedMap headers = jerseyResponse.getHeaders(); + List list = headers.get(headerName); + if (list == null) { + list = new ArrayList<>(); + } + list.add(header.getValue()); + headers.put(headerName, list); + } + } + + private static final class HttpClientResponseInputStream extends FilterInputStream { + + HttpClientResponseInputStream(final ContentResponse jettyResponse) throws IOException { + super(getInputStream(jettyResponse)); + } + + private static InputStream getInputStream(final ContentResponse response) { + return new ByteArrayInputStream(response.getContent()); + } + } + + private Request translateRequest(final ClientRequest clientRequest) { + + final URI uri = clientRequest.getUri(); + final Request request = client.newRequest(uri); + request.method(clientRequest.getMethod()); + + 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.idleTimeout((Integer) readTimeout, TimeUnit.MILLISECONDS); + } + + final Object totalTimeout = clientRequest.resolveProperty(Jetty11ClientProperties.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); + + // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version) + request.headers(httpFields -> httpFields.remove(HttpHeader.USER_AGENT)); + 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) { + final Object entity = clientRequest.getEntity(); + + if (entity == null) { + return null; + } + + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { + @Override + public OutputStream getOutputStream(final int contentLength) throws IOException { + return outputStream; + } + }); + + try { + clientRequest.writeEntity(); + } catch (final IOException e) { + throw new ProcessingException("Failed to write request entity.", e); + } + return new BytesContentProvider(outputStream.toByteArray()); + } + + private ContentProvider getStreamProvider(final ClientRequest clientRequest) { + final Object entity = clientRequest.getEntity(); + + if (entity == null) { + return null; + } + + final OutputStreamContentProvider streamContentProvider = new OutputStreamContentProvider(); + clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { + @Override + public OutputStream getOutputStream(final int contentLength) throws IOException { + return streamContentProvider.getOutputStream(); + } + }); + return streamContentProvider; + } + + private void processContent(final ClientRequest clientRequest, final ContentProvider entity) throws IOException { + if (entity == null) { + return; + } + + final OutputStreamContentProvider streamContentProvider = (OutputStreamContentProvider) entity; + try (final OutputStream output = streamContentProvider.getOutputStream()) { + clientRequest.writeEntity(); + } + } + + @Override + public Future apply(final ClientRequest jerseyRequest, final AsyncConnectorCallback callback) { + final Request jettyRequest = translateRequest(jerseyRequest); + final Map clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest); + final ContentProvider entity = getStreamProvider(jerseyRequest); + if (entity != null) { + jettyRequest.content(entity); + } + final AtomicBoolean callbackInvoked = new AtomicBoolean(false); + final Throwable failure; + try { + 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(); + jettyRequest.send(new Response.Listener.Adapter() { + + @Override + public void onHeaders(final Response jettyResponse) { + HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(), + Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration()); + + if (responseFuture.isDone()) { + if (!callbackInvoked.compareAndSet(false, true)) { + return; + } + } + final ClientResponse response = translateResponse(jerseyRequest, jettyResponse, entityStream); + jerseyResponse.set(response); + } + + @Override + public void onContent(final Response jettyResponse, final ByteBuffer content) { + try { + // content must be consumed before returning from this method. + + if (content.hasArray()) { + byte[] array = content.array(); + byte[] buff = new byte[content.remaining()]; + System.arraycopy(array, content.arrayOffset(), buff, 0, content.remaining()); + entityStream.put(ByteBuffer.wrap(buff)); + } else { + byte[] buff = new byte[content.remaining()]; + content.get(buff); + entityStream.put(ByteBuffer.wrap(buff)); + } + } catch (final InterruptedException ex) { + final ProcessingException pe = new ProcessingException(ex); + entityStream.closeQueue(pe); + // try to complete the future with an exception + responseFuture.completeExceptionally(pe); + Thread.currentThread().interrupt(); + } + } + + @Override + public void onComplete(final Result result) { + entityStream.closeQueue(); + if (!callbackInvoked.get()) { + callback.response(jerseyResponse.get()); + } + responseFuture.complete(jerseyResponse.get()); + } + + @Override + public void onFailure(final Response response, final Throwable t) { + entityStream.closeQueue(t); + // try to complete the future with an exception + responseFuture.completeExceptionally(t); + if (callbackInvoked.compareAndSet(false, true)) { + callback.failure(t); + } + } + }); + processContent(jerseyRequest, entity); + return responseFuture; + } catch (final Throwable t) { + failure = t; + } + + if (callbackInvoked.compareAndSet(false, true)) { + callback.failure(failure); + } + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(failure); + return future; + } + + private static ClientResponse translateResponse(final ClientRequest jerseyRequest, + final Response jettyResponse, + final NonBlockingInputStream entityStream) { + final ClientResponse jerseyResponse = new ClientResponse(Statuses.from(jettyResponse.getStatus()), jerseyRequest); + processResponseHeaders(jettyResponse.getHeaders(), jerseyResponse); + jerseyResponse.setEntityStream(entityStream); + return jerseyResponse; + } + + @Override + public String getName() { + return "Jetty HttpClient " + Jetty.VERSION; + } + + @Override + public void close() { + try { + client.stop(); + } catch (final Exception e) { + throw new ProcessingException("Failed to stop the client.", e); + } + } +} diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11ConnectorProvider.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11ConnectorProvider.java new file mode 100644 index 0000000000..de6bc77c9c --- /dev/null +++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11ConnectorProvider.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.jetty11.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 Jetty11ClientProperties#DISABLE_COOKIES}
  • * + *
  • {@link Jetty11ClientProperties#ENABLE_SSL_HOSTNAME_VERIFICATION}
  • + *
  • {@link Jetty11ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}
  • + *
  • {@link Jetty11ClientProperties#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 Jetty11ConnectorProvider implements ConnectorProvider { + + @Override + public Connector getConnector(Client client, Configuration runtimeConfig) { + if (JdkVersion.getJdkVersion().getMajor() < 11) { + throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED()); + } + return new Jetty11Connector(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 Jetty11Connector) { + return ((Jetty11Connector) connector).getHttpClient(); + } + + throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED()); + } +} diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11HttpClientContract.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11HttpClientContract.java new file mode 100644 index 0000000000..3b0321d0ad --- /dev/null +++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11HttpClientContract.java @@ -0,0 +1,33 @@ +/* + * 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.jetty11.connector; + +import org.eclipse.jetty.client.HttpClient; +import org.glassfish.jersey.spi.Contract; + +/** + * A contract that allows for an optional registration of user predefined Jetty {@code HttpClient} + * that is consequently used by {@link Jetty11Connector} + */ +@Contract +public interface Jetty11HttpClientContract { + /** + * Supply a user predefined HttpClient + * @return a user predefined HttpClient + */ + HttpClient getHttpClient(); +} diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11HttpClientSupplier.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11HttpClientSupplier.java new file mode 100644 index 0000000000..b5f1462cb1 --- /dev/null +++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11HttpClientSupplier.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.jetty11.connector; + +import org.eclipse.jetty.client.HttpClient; + +/** + * Jetty HttpClient supplier to be registered into Jersey configuration to be used by {@link Jetty11Connector}. + * 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 Jetty11Connector} the usual way. + *

+ */ +public class Jetty11HttpClientSupplier implements Jetty11HttpClientContract { + 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 Jetty11Connector#getHttpClient()} is called. + */ + public Jetty11HttpClientSupplier(HttpClient httpClient) { + this.httpClient = httpClient; + } + + @Override + public HttpClient getHttpClient() { + return httpClient; + } +} diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/package-info.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/package-info.java new file mode 100644 index 0000000000..0f85c4f693 --- /dev/null +++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/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.jetty11.connector; diff --git a/connectors/jetty11-connector/src/main/resources/org/glassfish/jersey/jetty11/connector/localization.properties b/connectors/jetty11-connector/src/main/resources/org/glassfish/jersey/jetty11/connector/localization.properties new file mode 100644 index 0000000000..aacb267211 --- /dev/null +++ b/connectors/jetty11-connector/src/main/resources/org/glassfish/jersey/jetty11/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/jetty11/connector/AsyncTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/AsyncTest.java new file mode 100644 index 0000000000..9d0edbc556 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + /** + * 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/jetty11/connector/AuthFilterTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/AuthFilterTest.java new file mode 100644 index 0000000000..1c6cddcb9d --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + @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/jetty11/connector/AuthTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/AuthTest.java new file mode 100644 index 0000000000..6b5111b1dd --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new Jetty11ConnectorProvider()); + 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(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new Jetty11ConnectorProvider()); + 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(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, + new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password")); + config.connectorProvider(new Jetty11ConnectorProvider()); + 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/jetty11/connector/CookieTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/CookieTest.java new file mode 100644 index 0000000000..0cd585e9db --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + 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(Jetty11ClientProperties.DISABLE_COOKIES, true); + cc.connectorProvider(new Jetty11ConnectorProvider()); + 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 Jetty11Connector connector = (Jetty11Connector) 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 Jetty11ConnectorProvider()); + 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 Jetty11Connector connector = (Jetty11Connector) 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/jetty11/connector/CustomLoggingFilter.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/CustomLoggingFilter.java new file mode 100644 index 0000000000..386f96e05e --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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/jetty11/connector/EntityTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/EntityTest.java new file mode 100644 index 0000000000..e9de3530ec --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + //.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/jetty11/connector/ErrorTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/ErrorTest.java new file mode 100644 index 0000000000..a8a97e5f69 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + + @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/jetty11/connector/FollowRedirectsTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/FollowRedirectsTest.java new file mode 100644 index 0000000000..a30efbc87e --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + 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 Jetty11ConnectorProvider()); + 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 Jetty11ConnectorProvider()); + 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/jetty11/connector/GZIPContentEncodingTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/GZIPContentEncodingTest.java new file mode 100644 index 0000000000..459eccb3f1 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + @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 Jetty11ConnectorProvider()); + 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/jetty11/connector/HelloWorldTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/HelloWorldTest.java new file mode 100644 index 0000000000..a60719b523 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + @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/jetty11/connector/HttpHeadersTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/HttpHeadersTest.java new file mode 100644 index 0000000000..3d03872fd8 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + @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/jetty11/connector/ManagedClientTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/ManagedClientTest.java new file mode 100644 index 0000000000..628af896d7 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + /** + * 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/jetty11/connector/MethodTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/MethodTest.java new file mode 100644 index 0000000000..f62834b8d8 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + @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/jetty11/connector/NoEntityTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/NoEntityTest.java new file mode 100644 index 0000000000..f5f754d276 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + @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/jetty11/connector/SyncResponseSizeTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/SyncResponseSizeTest.java new file mode 100644 index 0000000000..be67e9a363 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + @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 Jetty11ConnectorProvider()); + + 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 Jetty11ConnectorProvider()); + config.property(Jetty11ClientProperties.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 Jetty11ConnectorProvider()); + config.property(Jetty11ClientProperties.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/jetty11/connector/TimeoutTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/TimeoutTest.java new file mode 100644 index 0000000000..364426238f --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider()); + } + + @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 Jetty11ConnectorProvider()); + 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 Jetty11ConnectorProvider()); + 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(Jetty11ClientProperties.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/jetty11/connector/TraceSupportTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/TraceSupportTest.java new file mode 100644 index 0000000000..751f1273a5 --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider())); + } + + + 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/jetty11/connector/UnderlyingHttpClientAccessTest.java b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/connector/UnderlyingHttpClientAccessTest.java new file mode 100644 index 0000000000..c1a647043c --- /dev/null +++ b/connectors/jetty11-connector/src/test/java/org/glassfish/jersey/jetty11/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.jetty11.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 Jetty11ConnectorProvider())); + final HttpClient hcOnClient = Jetty11ConnectorProvider.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 = Jetty11ConnectorProvider.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 Jetty11ConnectorProvider()) + .register(new Jetty11HttpClientSupplier(httpClient)); + final Client client = ClientBuilder.newClient(clientConfig); + final WebTarget target = client.target("http://localhost/"); + final HttpClient hcOnTarget = Jetty11ConnectorProvider.getHttpClient(target); + + assertThat("Instance provided to a ClientConfig differs from instance provided by JettyProvider", + httpClient, is(hcOnTarget)); + } +} diff --git a/connectors/pom.xml b/connectors/pom.xml index 0c2dcb4e3e..3ac604039b 100644 --- a/connectors/pom.xml +++ b/connectors/pom.xml @@ -40,6 +40,7 @@ helidon-connector jdk-connector jetty-connector + jetty11-connector jnh-connector netty-connector diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml index d8dad09831..5cfadb2109 100644 --- a/docs/src/main/docbook/appendix-properties.xml +++ b/docs/src/main/docbook/appendix-properties.xml @@ -1121,7 +1121,8 @@ &jersey.apache5.Apache5ConnectorProvider;, &jersey.grizzly.GrizzlyConnectorProvider;, &jersey.helidon.HelidonConnectorProvider;, - &jersey.netty.NettyConnectorProvider;, and + &jersey.netty.NettyConnectorProvider;, + &jersey.jetty11.Jetty11ConnectorProvider;, and &jersey.jetty.JettyConnectorProvider; only. diff --git a/docs/src/main/docbook/client.xml b/docs/src/main/docbook/client.xml index 4db30100bd..fb449e2ff6 100644 --- a/docs/src/main/docbook/client.xml +++ b/docs/src/main/docbook/client.xml @@ -656,10 +656,15 @@ webTarget.request().post(Entity.entity(f, MediaType.TEXT_PLAIN_TYPE)); org.glassfish.jersey.connectors:jersey-helidon-connector - Jetty HTTP client + Jetty HTTP client (JDK 17+) &jersey.jetty.JettyConnectorProvider; org.glassfish.jersey.connectors:jersey-jetty-connector + + Jetty 11.x HTTP client + &jersey.jetty11.Jetty11ConnectorProvider; + org.glassfish.jersey.connectors:jersey-jetty11-connector + Netty NIO framework &jersey.netty.NettyConnectorProvider; @@ -858,6 +863,21 @@ Client client = ClientBuilder.newClient(clientConfig); +
+ Jetty 11.x HttpClient Configuration + + For Jetty Connector, an &jersey.jetty11.Jetty11HttpClientSupplier; SPI allows for providing a configured instance + of org.eclipse.jetty.client.HttpClient: + + HttpClient httpClient = new HttpClient(...); + ClientConfig clientConfig = new ClientConfig() + .connectorProvider(new Jetty11ConnectorProvider()) + .register(new Jetty11HttpClientSupplier(httpClient)); + Client client = ClientBuilder.newClient(clientConfig); + ... + + +
@@ -1008,9 +1028,9 @@ Client client = ClientBuilder.newBuilder().sslContext(sslContext).build(); diff --git a/docs/src/main/docbook/dependencies.xml b/docs/src/main/docbook/dependencies.xml index 94c62e8d08..97325dcef2 100644 --- a/docs/src/main/docbook/dependencies.xml +++ b/docs/src/main/docbook/dependencies.xml @@ -1,7 +1,7 @@ +<dependency> + <groupId>org.glassfish.jersey.connectory</groupId> + <artifactId>jersey-jetty11-connector</artifactId> + <version>&version;</version> +</dependency> + <dependency> <groupId>org.glassfish.jersey.connectors</groupId> <artifactId>jersey-jetty-connector</artifactId> diff --git a/docs/src/main/docbook/jersey.ent b/docs/src/main/docbook/jersey.ent index 4a81897979..4daa8083ce 100644 --- a/docs/src/main/docbook/jersey.ent +++ b/docs/src/main/docbook/jersey.ent @@ -487,6 +487,14 @@ JettyHttpContainerFactory"> JettyHttpContainerProvider"> JettyWebContainerFactory"> +Jetty11ClientProperties" > +Jetty11HttpClientSupplier" > +Jetty11ClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION" > +Jetty11ClientProperties.DISABLE_COOKIES" > +Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION" > +Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE" > +Jetty11ClientProperties.TOTAL_TIMEOUT" > +Jetty11ConnectorProvider"> JavaNetHttpConnectorProvider"> JavaNetHttpClientProperties"> JavaNetHttpClientProperties.COOKIE_HANDLER"> @@ -996,6 +1004,7 @@ JettyHttpContainerFactory"> JettyHttpContainerProvider"> JettyWebContainerFactory"> +Jetty11ConnectorProvider"> DeclarativeLinkingFeature"> LoggingFeature"> LoggingFeature.DEFAULT_LOGGER_NAME"> diff --git a/docs/src/main/docbook/modules.xml b/docs/src/main/docbook/modules.xml index c7deae96af..e83fbd4f24 100644 --- a/docs/src/main/docbook/modules.xml +++ b/docs/src/main/docbook/modules.xml @@ -1,7 +1,7 @@