From cfe34136587f5d917151ce974e0c2faa1ddda98f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Steffen=20Nie=C3=9Fing?=
Date: Sat, 18 Dec 2021 18:12:38 +0100
Subject: [PATCH] Issue #4881 - Java client connector
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Provide a basic implementation of a client connector using java.net.http.HttpClient
Signed-off-by: Steffen Nießing
---
bom/pom.xml | 5 +
connectors/java-connector/pom.xml | 79 ++++++
.../java/connector/JavaClientProperties.java | 51 ++++
.../jersey/java/connector/JavaConnector.java | 234 ++++++++++++++++++
.../java/connector/JavaConnectorProvider.java | 76 ++++++
.../jersey/java/connector/package-info.java | 21 ++
.../java/connector/localization.properties | 21 ++
.../connector/AbstractJavaConnectorTest.java | 128 ++++++++++
.../jersey/java/connector/AsyncTest.java | 72 ++++++
.../java/connector/BodyPublisherTest.java | 59 +++++
.../java/connector/OptionsMethodTest.java | 36 +++
.../jersey/java/connector/RedirectTest.java | 48 ++++
...veHttpClientFromConnectorProviderTest.java | 40 +++
connectors/pom.xml | 1 +
pom.xml | 13 +
15 files changed, 884 insertions(+)
create mode 100644 connectors/java-connector/pom.xml
create mode 100644 connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaClientProperties.java
create mode 100644 connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaConnector.java
create mode 100644 connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaConnectorProvider.java
create mode 100644 connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/package-info.java
create mode 100644 connectors/java-connector/src/main/resources/org/glassfish/jersey/java/connector/localization.properties
create mode 100644 connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/AbstractJavaConnectorTest.java
create mode 100644 connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/AsyncTest.java
create mode 100644 connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/BodyPublisherTest.java
create mode 100644 connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/OptionsMethodTest.java
create mode 100644 connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/RedirectTest.java
create mode 100644 connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/RetrieveHttpClientFromConnectorProviderTest.java
diff --git a/bom/pom.xml b/bom/pom.xml
index a54b744c74..d2466280d7 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -73,6 +73,11 @@
jersey-grizzly-connector
${project.version}
+
+ org.glassfish.jersey.connectors
+ jersey-java-connector
+ ${project.version}
+
org.glassfish.jersey.connectors
jersey-jetty-connector
diff --git a/connectors/java-connector/pom.xml b/connectors/java-connector/pom.xml
new file mode 100644
index 0000000000..c2f1ef073f
--- /dev/null
+++ b/connectors/java-connector/pom.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+ project
+ org.glassfish.jersey.connectors
+ 3.1.0-SNAPSHOT
+
+ 4.0.0
+
+ jersey-java-connector
+ jar
+ jersey-connectors-java
+
+ Jersey Client Transport via Java's HttpClient
+
+
+ UTF-8
+
+
+
+
+ org.glassfish.jersey.test-framework.providers
+ jersey-test-framework-provider-bundle
+ ${project.version}
+ pom
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+
+
+
+ com.sun.istack
+ istack-commons-maven-plugin
+ true
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ true
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaClientProperties.java b/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaClientProperties.java
new file mode 100644
index 0000000000..cb8f60e8c2
--- /dev/null
+++ b/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaClientProperties.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import org.glassfish.jersey.internal.util.PropertiesClass;
+
+import java.net.http.HttpClient;
+
+/**
+ * Provides configuration properties for a {@link JavaConnector}.
+ *
+ * @author Steffen Nießing
+ */
+@PropertiesClass
+public class JavaClientProperties {
+ /**
+ * Configuration of the {@link java.net.CookieHandler} that should be used by the {@link HttpClient}.
+ * If this option is not set, {@link HttpClient#cookieHandler()} will return an empty {@link java.util.Optional}
+ * and therefore no cookie handler will be used.
+ *
+ * A provided value to this option has to be of type {@link java.net.CookieHandler}.
+ */
+ public static final String COOKIE_HANDLER = "jersey.config.java.client.cookieHandler";
+
+ /**
+ * Configuration of SSL parameters used by the {@link HttpClient}.
+ * If this option is not set, then the {@link HttpClient} will use implementation specific default values.
+ *
+ * A provided value to this option has to be of type {@link javax.net.ssl.SSLParameters}.
+ */
+ public static final String SSL_PARAMETERS = "jersey.config.java.client.sslParameters";
+
+ /**
+ * Prevent this class from instantiation.
+ */
+ private JavaClientProperties() {}
+}
diff --git a/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaConnector.java b/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaConnector.java
new file mode 100644
index 0000000000..9c104abc69
--- /dev/null
+++ b/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaConnector.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+import jakarta.ws.rs.core.MultivaluedMap;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.internal.Version;
+import org.glassfish.jersey.message.internal.OutboundMessageContext;
+import org.glassfish.jersey.message.internal.Statuses;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CookieHandler;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.logging.Logger;
+
+/**
+ * Provides a Jersey client {@link Connector}, which internally uses Java's {@link HttpClient}.
+ * The following properties are provided to Java's {@link HttpClient.Builder} during creation of the {@link HttpClient}:
+ *
+ * - {@link ClientProperties#CONNECT_TIMEOUT}
+ * - {@link ClientProperties#FOLLOW_REDIRECTS}
+ * - {@link JavaClientProperties#COOKIE_HANDLER}
+ * - {@link JavaClientProperties#SSL_PARAMETERS}
+ *
+ *
+ * @author Steffen Nießing
+ */
+public class JavaConnector implements Connector {
+ private static final Logger LOGGER = Logger.getLogger(JavaConnector.class.getName());
+
+ private final HttpClient httpClient;
+
+ /**
+ * Constructs a new {@link Connector} for a Jersey client instance using Java's {@link HttpClient}.
+ *
+ * @param client a Jersey client instance to get additional configuration properties from (e.g. {@link SSLContext})
+ * @param configuration the configuration properties for this connector
+ */
+ public JavaConnector(final Client client, final Configuration configuration) {
+ HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
+ httpClientBuilder.version(HttpClient.Version.HTTP_1_1);
+ SSLContext sslContext = client.getSslContext();
+ if (sslContext != null) {
+ httpClientBuilder.sslContext(sslContext);
+ }
+ Integer connectTimeout = getPropertyOrNull(configuration, ClientProperties.CONNECT_TIMEOUT, Integer.class);
+ if (connectTimeout != null) {
+ httpClientBuilder.connectTimeout(Duration.of(connectTimeout, ChronoUnit.MILLIS));
+ }
+ CookieHandler cookieHandler = getPropertyOrNull(configuration, JavaClientProperties.COOKIE_HANDLER, CookieHandler.class);
+ if (cookieHandler != null) {
+ httpClientBuilder.cookieHandler(cookieHandler);
+ }
+ Boolean redirect = getPropertyOrNull(configuration, ClientProperties.FOLLOW_REDIRECTS, Boolean.class);
+ if (redirect != null) {
+ httpClientBuilder.followRedirects(redirect ? HttpClient.Redirect.ALWAYS : HttpClient.Redirect.NEVER);
+ } else {
+ httpClientBuilder.followRedirects(HttpClient.Redirect.NORMAL);
+ }
+ SSLParameters sslParameters = getPropertyOrNull(configuration, JavaClientProperties.SSL_PARAMETERS, SSLParameters.class);
+ if (sslParameters != null) {
+ httpClientBuilder.sslParameters(sslParameters);
+ }
+ this.httpClient = httpClientBuilder.build();
+ }
+
+ /**
+ * Implements a {@link org.glassfish.jersey.message.internal.OutboundMessageContext.StreamProvider}
+ * for a {@link ByteArrayOutputStream}.
+ */
+ private static class ByteArrayOutputStreamProvider implements OutboundMessageContext.StreamProvider {
+ private ByteArrayOutputStream byteArrayOutputStream;
+
+ public ByteArrayOutputStream getByteArrayOutputStream() {
+ return byteArrayOutputStream;
+ }
+
+ @Override
+ public OutputStream getOutputStream(int contentLength) throws IOException {
+ return this.byteArrayOutputStream = new ByteArrayOutputStream(contentLength);
+ }
+ }
+
+ /**
+ * Builds a request for the {@link HttpClient} from Jersey's {@link ClientRequest}.
+ *
+ * @param request the Jersey request to get request data from
+ * @return the {@link HttpRequest} instance for the {@link HttpClient} request
+ */
+ private HttpRequest getHttpRequest(ClientRequest request) {
+ HttpRequest.Builder builder = HttpRequest.newBuilder();
+ builder.uri(request.getUri());
+ HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.noBody();
+ if (request.hasEntity()) {
+ try {
+ request.enableBuffering();
+ ByteArrayOutputStreamProvider byteBufferStreamProvider = new ByteArrayOutputStreamProvider();
+ request.setStreamProvider(byteBufferStreamProvider);
+ request.writeEntity();
+ bodyPublisher = HttpRequest.BodyPublishers.ofByteArray(
+ byteBufferStreamProvider.getByteArrayOutputStream().toByteArray()
+ );
+ } catch (IOException e) {
+ throw new ProcessingException(LocalizationMessages.ERROR_INVALID_ENTITY(), e);
+ }
+ }
+ builder.method(request.getMethod(), bodyPublisher);
+ for (Map.Entry> entry : request.getRequestHeaders().entrySet()) {
+ String headerName = entry.getKey();
+ for (String headerValue : entry.getValue()) {
+ builder.header(headerName, headerValue);
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Retrieves a property from the configuration, if it was provided.
+ *
+ * @param configuration the {@link Configuration} to get the property information from
+ * @param propertyKey the name of the property to retrieve
+ * @param resultClass the type to which the property value should be case
+ * @param the generic type parameter of the result type
+ * @return the requested property or {@code null}, if it was not provided or has the wrong type
+ */
+ @SuppressWarnings("unchecked")
+ private T getPropertyOrNull(final Configuration configuration, final String propertyKey, final Class resultClass) {
+ Object propertyObject = configuration.getProperty(propertyKey);
+ if (propertyObject == null) {
+ return null;
+ }
+ if (!resultClass.isInstance(propertyObject)) {
+ LOGGER.warning(LocalizationMessages.ERROR_INVALID_CLASS(propertyKey, resultClass.getName()));
+ return null;
+ }
+ return (T) propertyObject;
+ }
+
+ /**
+ * Translates a {@link HttpResponse} from the {@link HttpClient} to a Jersey {@link ClientResponse}.
+ *
+ * @param request the {@link ClientRequest} to get additional information (e.g. header values) from
+ * @param response the {@link HttpClient} response object
+ * @return the translated Jersey {@link ClientResponse} object
+ */
+ private ClientResponse buildClientResponse(ClientRequest request, HttpResponse response) {
+ ClientResponse clientResponse = new ClientResponse(Statuses.from(response.statusCode()), request);
+ MultivaluedMap headers = clientResponse.getHeaders();
+ for (Map.Entry> entry : response.headers().map().entrySet()) {
+ String headerName = entry.getKey();
+ if (headers.get(headerName) != null) {
+ headers.get(headerName).addAll(entry.getValue());
+ } else {
+ headers.put(headerName, entry.getValue());
+ }
+ }
+ clientResponse.setEntityStream(response.body());
+ return clientResponse;
+ }
+
+ /**
+ * Returns the underlying {@link HttpClient} instance used by this connector.
+ *
+ * @return the Java {@link HttpClient} instance
+ */
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+
+ @Override
+ public ClientResponse apply(ClientRequest request) {
+ HttpRequest httpRequest = getHttpRequest(request);
+ try {
+ HttpResponse response = this.httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofInputStream());
+ return buildClientResponse(request, response);
+ } catch (IOException | InterruptedException e) {
+ throw new ProcessingException(e);
+ }
+ }
+
+ @Override
+ public Future> apply(ClientRequest request, AsyncConnectorCallback callback) {
+ HttpRequest httpRequest = getHttpRequest(request);
+ CompletableFuture response = this.httpClient
+ .sendAsync(httpRequest, HttpResponse.BodyHandlers.ofInputStream())
+ .thenApply(httpResponse -> buildClientResponse(request, httpResponse));
+ response.thenAccept(callback::response);
+ return response;
+ }
+
+ @Override
+ public String getName() {
+ return "Java HttpClient Connector " + Version.getVersion();
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaConnectorProvider.java b/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaConnectorProvider.java
new file mode 100644
index 0000000000..1fa38ab056
--- /dev/null
+++ b/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/JavaConnectorProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configurable;
+import jakarta.ws.rs.core.Configuration;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.Initializable;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+
+import java.net.http.HttpClient;
+
+/**
+ * A provider class for a Jersey client {@link Connector} using Java's {@link HttpClient}.
+ *
+ * The following configuration properties are available:
+ *
+ * - {@link ClientProperties#CONNECT_TIMEOUT}
+ * - {@link ClientProperties#FOLLOW_REDIRECTS} (defaults to {@link java.net.http.HttpClient.Redirect#NORMAL} when unset)
+ * - {@link JavaClientProperties#COOKIE_HANDLER}
+ * - {@link JavaClientProperties#SSL_PARAMETERS}
+ *
+ *
+ *
+ * @author Steffen Nießing
+ */
+public class JavaConnectorProvider implements ConnectorProvider {
+ @Override
+ public Connector getConnector(Client client, Configuration runtimeConfig) {
+ return new JavaConnector(client, runtimeConfig);
+ }
+
+ /**
+ * Retrieve the Java {@link HttpClient} used by the provided {@link JavaConnector}.
+ *
+ * @param component the component from which the {@link JavaConnector} should be retrieved
+ * @return a Java {@link HttpClient} instance
+ * @throws java.lang.IllegalArgumentException if a {@link JavaConnector} cannot be provided from the given {@code component}
+ */
+ public static HttpClient getHttpClient(Configurable> component) {
+ try {
+ final Initializable> initializable = (Initializable>) component;
+
+ Connector connector = initializable.getConfiguration().getConnector() != null
+ ? initializable.getConfiguration().getConnector()
+ : initializable.preInitialize().getConfiguration().getConnector();
+
+ if (connector instanceof JavaConnector) {
+ return ((JavaConnector) connector).getHttpClient();
+ } else {
+ throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED());
+ }
+ } catch (ClassCastException classCastException) {
+ throw new IllegalArgumentException(
+ LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName()),
+ classCastException
+ );
+ }
+ }
+}
diff --git a/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/package-info.java b/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/package-info.java
new file mode 100644
index 0000000000..e50e0bb927
--- /dev/null
+++ b/connectors/java-connector/src/main/java/org/glassfish/jersey/java/connector/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2021 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
+ * Java's {@link java.net.http.HttpClient}.
+ */
+package org.glassfish.jersey.java.connector;
\ No newline at end of file
diff --git a/connectors/java-connector/src/main/resources/org/glassfish/jersey/java/connector/localization.properties b/connectors/java-connector/src/main/resources/org/glassfish/jersey/java/connector/localization.properties
new file mode 100644
index 0000000000..ca98e99961
--- /dev/null
+++ b/connectors/java-connector/src/main/resources/org/glassfish/jersey/java/connector/localization.properties
@@ -0,0 +1,21 @@
+#
+# Copyright (c) 2021 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
+#
+
+error.body.publisher=Could not determine BodyPublisher for entity.
+error.invalid.class={0} is not an instance of {1}. Ignoring property.
+error.invalid.entity=Could not serialize entity.
+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 JavaConnectorProvider.
diff --git a/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/AbstractJavaConnectorTest.java b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/AbstractJavaConnectorTest.java
new file mode 100644
index 0000000000..7d3980d208
--- /dev/null
+++ b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/AbstractJavaConnectorTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+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 java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * An abstract base class for tests of the {@link JavaConnector} providing common resources and utility methods.
+ */
+abstract class AbstractJavaConnectorTest extends JerseyTest {
+ private static final Logger LOGGER = Logger.getLogger(AbstractJavaConnectorTest.class.getName());
+ public static final String RESOURCE_PATH = "java-connector";
+
+ @Path(RESOURCE_PATH)
+ public static class JavaConnectorTestResource {
+ @GET
+ public String helloWorld() {
+ return "Hello World!";
+ }
+
+ @GET
+ @Path("redirect")
+ public Response redirectToHelloWorld() throws URISyntaxException {
+ return Response.seeOther(new URI(RESOURCE_PATH)).build();
+ }
+
+ @POST
+ @Path("echo")
+ @Produces(MediaType.TEXT_PLAIN)
+ @Consumes(MediaType.TEXT_PLAIN)
+ public String echo(String entity) {
+ return entity;
+ }
+
+ @POST
+ @Path("echo-byte-array")
+ @Produces(MediaType.APPLICATION_OCTET_STREAM)
+ @Consumes(MediaType.APPLICATION_OCTET_STREAM)
+ public byte[] echoByteArray(byte[] byteArray) {
+ return byteArray;
+ }
+
+ @POST
+ @Path("async")
+ public void asyncPostWithTimeout(@QueryParam("timeout") @DefaultValue("10") Long timeoutSeconds,
+ @Suspended final AsyncResponse asyncResponse,
+ String message) {
+ asyncResponse.setTimeoutHandler(asyncResponse1 ->
+ asyncResponse1.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Timeout").build()));
+ asyncResponse.setTimeout(timeoutSeconds, TimeUnit.SECONDS);
+ CompletableFuture.runAsync(() -> {
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
+ })
+ .handleAsync((unused, throwable) -> throwable != null ? "INTERRUPTED" : message)
+ .thenApplyAsync(asyncResponse::resume);
+ }
+ }
+
+ protected Response request(String path) {
+ return target().path(path).request().get();
+ }
+
+ protected Response requestWithEntity(String path, String method, Entity> entity) {
+ return target().path(path).request().method(method, entity);
+ }
+
+ protected Future requestAsync(String path) {
+ return target().path(path).request().async().get();
+ }
+
+ protected Future requestAsyncWithEntity(String path, String method, Entity> entity) {
+ return target().path(path).request().async().method(method, entity);
+ }
+
+ @Override
+ protected Application configure() {
+ return new ResourceConfig(JavaConnectorTestResource.class)
+ .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ }
+
+ @Override
+ protected void configureClient(ClientConfig config) {
+ config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+ config.connectorProvider(new JavaConnectorProvider());
+ }
+}
diff --git a/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/AsyncTest.java b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/AsyncTest.java
new file mode 100644
index 0000000000..dc0852169b
--- /dev/null
+++ b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/AsyncTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Response;
+import org.junit.Test;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Tests asynchronous and interleaved requests.
+ */
+public class AsyncTest extends AbstractJavaConnectorTest {
+ /**
+ * Checks, that 3 interleaved requests all complete and return their associated responses.
+ * Additionally checks, that all requests complete in 3 times the running time on the server.
+ */
+ @Test
+ public void testAsyncRequestsWithoutTimeout() {
+ Future request1 = this.requestAsyncWithEntity("java-connector/async", "POST", Entity.text("request1"));
+ Future request2 = this.requestAsyncWithEntity("java-connector/async", "POST", Entity.text("request2"));
+ Future request3 = this.requestAsyncWithEntity("java-connector/async", "POST", Entity.text("request3"));
+
+ assertThatCode(() -> {
+ // wait 3 times the processing time and throw if not completed until then
+ await().atMost(3 * 3000, TimeUnit.MILLISECONDS)
+ .until(() -> request1.isDone() && request2.isDone() && request3.isDone());
+ String response1 = request1.get().readEntity(String.class);
+ String response2 = request2.get().readEntity(String.class);
+ String response3 = request3.get().readEntity(String.class);
+ assertThat(response1).isEqualTo("request1");
+ assertThat(response2).isEqualTo("request2");
+ assertThat(response3).isEqualTo("request3");
+ }).doesNotThrowAnyException();
+ }
+
+ /**
+ * Checks, that a status {@link Response.Status#SERVICE_UNAVAILABLE} is thrown, if a request computes too long.
+ */
+ @Test
+ public void testAsyncRequestsWithTimeout() throws ExecutionException, InterruptedException {
+ try {
+ Response response = target().path("java-connector").path("async").queryParam("timeout", 1)
+ .request().async().post(Entity.text("")).get();
+ assertThat(response.getStatus()).isEqualTo(Response.Status.SERVICE_UNAVAILABLE.getStatusCode());
+ assertThat(response.readEntity(String.class)).isEqualTo("Timeout");
+ } catch (InterruptedException | ExecutionException ex) {
+ throw new RuntimeException("Could not correctly get response", ex);
+ }
+ }
+}
diff --git a/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/BodyPublisherTest.java b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/BodyPublisherTest.java
new file mode 100644
index 0000000000..dbbbe8251a
--- /dev/null
+++ b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/BodyPublisherTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Checks, that request entities are correctly serialized and deserialized.
+ */
+public class BodyPublisherTest extends AbstractJavaConnectorTest {
+ /**
+ * Checks with a simple plain text entity.
+ */
+ @Test
+ public void testStringEntity() {
+ Response response = this.requestWithEntity("java-connector/echo", "POST", Entity.text("Echo"));
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatCode(() -> {
+ assertThat(response.readEntity(String.class)).isEqualTo("Echo");
+ }).doesNotThrowAnyException();
+ }
+
+ /**
+ * Checks with an octet stream entity.
+ */
+ @Test
+ public void testByteArrayEntity() {
+ String test = "test-string";
+ Response response = this.requestWithEntity("java-connector/echo-byte-array", "POST",
+ Entity.entity(test.getBytes(StandardCharsets.UTF_8), MediaType.APPLICATION_OCTET_STREAM_TYPE));
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatCode(() -> {
+ assertThat(response.readEntity(byte[].class))
+ .satisfies(bytes -> assertThat(new String(bytes, StandardCharsets.UTF_8)).isEqualTo("test-string"));
+ }).doesNotThrowAnyException();
+ }
+}
diff --git a/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/OptionsMethodTest.java b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/OptionsMethodTest.java
new file mode 100644
index 0000000000..27ec12525f
--- /dev/null
+++ b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/OptionsMethodTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import jakarta.ws.rs.core.Response;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Checks, that an {@code OPTIONS} request may be sent to a {@code GET} endpoint.
+ */
+public class OptionsMethodTest extends AbstractJavaConnectorTest {
+ /**
+ * Sends an {@code OPTIONS} request to the root {@code GET} endpoint and assumes a code 200.
+ */
+ @Test
+ public void testOptionsMethod() {
+ assertThat(this.requestWithEntity("java-connector", "OPTIONS", null).getStatus())
+ .isEqualTo(Response.Status.OK.getStatusCode());
+ }
+}
diff --git a/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/RedirectTest.java b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/RedirectTest.java
new file mode 100644
index 0000000000..1cd0c3d537
--- /dev/null
+++ b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/RedirectTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import jakarta.ws.rs.core.Response;
+import org.glassfish.jersey.client.ClientProperties;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Checks that the connector provider correctly handles redirects.
+ */
+public class RedirectTest extends AbstractJavaConnectorTest {
+ /**
+ * Checks, that without further configuration redirects are taken.
+ */
+ @Test
+ public void testRedirect() {
+ assertThat(this.request("java-connector/redirect").readEntity(String.class)).isEqualTo("Hello World!");
+ }
+
+ /**
+ * Checks, that no redirect happens, if the redirects are switched off.
+ */
+ @Test
+ public void testNotFollowRedirects() {
+ Response response = target().path("java-connector").path("redirect")
+ .property(ClientProperties.FOLLOW_REDIRECTS, false)
+ .request()
+ .get();
+ assertThat(response.getStatus()).isEqualTo(Response.Status.SEE_OTHER.getStatusCode());
+ }
+}
diff --git a/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/RetrieveHttpClientFromConnectorProviderTest.java b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/RetrieveHttpClientFromConnectorProviderTest.java
new file mode 100644
index 0000000000..aa831de34e
--- /dev/null
+++ b/connectors/java-connector/src/test/java/org/glassfish/jersey/java/connector/RetrieveHttpClientFromConnectorProviderTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2021 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.java.connector;
+
+import org.junit.Test;
+
+import java.net.http.HttpClient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests access to the {@link HttpClient} instance provided from the {@link JavaConnectorProvider}.
+ */
+public class RetrieveHttpClientFromConnectorProviderTest extends AbstractJavaConnectorTest {
+ /**
+ * Checks, that the {@link jakarta.ws.rs.client.Client} and {@link jakarta.ws.rs.client.WebTarget} instances
+ * correctly return the internally used {@link HttpClient}.
+ */
+ @Test
+ public void testClientUsesJavaConnector() {
+ assertThat(JavaConnectorProvider.getHttpClient(client())).isInstanceOf(HttpClient.class);
+ assertThat(JavaConnectorProvider.getHttpClient(target())).isInstanceOf(HttpClient.class);
+ assertThat(JavaConnectorProvider.getHttpClient(client()))
+ .isEqualTo(JavaConnectorProvider.getHttpClient(target()));
+ }
+}
diff --git a/connectors/pom.xml b/connectors/pom.xml
index 332ec68765..7592ef17d2 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -39,6 +39,7 @@
jdk-connector
jetty-connector
netty-connector
+ java-connector
diff --git a/pom.xml b/pom.xml
index d74e17d32f..dc5d3422da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1949,6 +1949,19 @@
6.9.6
test
+
+ org.assertj
+ assertj-core
+ 3.21.0
+ test
+
+
+
+ org.awaitility
+ awaitility
+ 4.1.1
+ test
+
org.hamcrest