Issue #4881 - Java client connector
Provide a basic implementation of a client connector using java.net.http.HttpClient
Signed-off-by: Steffen Nießing <zuniquex@protonmail.com>
diff --git a/bom/pom.xml b/bom/pom.xml
index a54b744..d246628 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -75,6 +75,11 @@
</dependency>
<dependency>
<groupId>org.glassfish.jersey.connectors</groupId>
+ <artifactId>jersey-java-connector</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.connectors</groupId>
<artifactId>jersey-jetty-connector</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/connectors/java-connector/pom.xml b/connectors/java-connector/pom.xml
new file mode 100644
index 0000000..c2f1ef0
--- /dev/null
+++ b/connectors/java-connector/pom.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>project</artifactId>
+ <groupId>org.glassfish.jersey.connectors</groupId>
+ <version>3.1.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>jersey-java-connector</artifactId>
+ <packaging>jar</packaging>
+ <name>jersey-connectors-java</name>
+
+ <description>Jersey Client Transport via Java's HttpClient</description>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+ <artifactId>jersey-test-framework-provider-bundle</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.sun.istack</groupId>
+ <artifactId>istack-commons-maven-plugin</artifactId>
+ <inherited>true</inherited>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <inherited>true</inherited>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ 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 0000000..cb8f60e
--- /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 <it>implementation specific</it> 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 0000000..9c104ab
--- /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}:
+ * <ul>
+ * <li>{@link ClientProperties#CONNECT_TIMEOUT}</li>
+ * <li>{@link ClientProperties#FOLLOW_REDIRECTS}</li>
+ * <li>{@link JavaClientProperties#COOKIE_HANDLER}</li>
+ * <li>{@link JavaClientProperties#SSL_PARAMETERS}</li>
+ * </ul>
+ *
+ * @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<String, List<String>> 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 <T> 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> T getPropertyOrNull(final Configuration configuration, final String propertyKey, final Class<T> 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<InputStream> response) {
+ ClientResponse clientResponse = new ClientResponse(Statuses.from(response.statusCode()), request);
+ MultivaluedMap<String, String> headers = clientResponse.getHeaders();
+ for (Map.Entry<String, List<String>> 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<InputStream> 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<ClientResponse> 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 0000000..1fa38ab
--- /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}.
+ * <p>
+ * The following configuration properties are available:
+ * <ul>
+ * <li>{@link ClientProperties#CONNECT_TIMEOUT}</li>
+ * <li>{@link ClientProperties#FOLLOW_REDIRECTS} (defaults to {@link java.net.http.HttpClient.Redirect#NORMAL} when unset)</li>
+ * <li>{@link JavaClientProperties#COOKIE_HANDLER}</li>
+ * <li>{@link JavaClientProperties#SSL_PARAMETERS}</li>
+ * </ul>
+ * </p>
+ *
+ * @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 0000000..e50e0bb
--- /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 0000000..ca98e99
--- /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 0000000..7d3980d
--- /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<Response> requestAsync(String path) {
+ return target().path(path).request().async().get();
+ }
+
+ protected Future<Response> 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 0000000..dc08521
--- /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<Response> request1 = this.requestAsyncWithEntity("java-connector/async", "POST", Entity.text("request1"));
+ Future<Response> request2 = this.requestAsyncWithEntity("java-connector/async", "POST", Entity.text("request2"));
+ Future<Response> 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 0000000..dbbbe82
--- /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 0000000..27ec125
--- /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 0000000..1cd0c3d
--- /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 0000000..aa831de
--- /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 332ec68..7592ef1 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -39,6 +39,7 @@
<module>jdk-connector</module>
<module>jetty-connector</module>
<module>netty-connector</module>
+ <module>java-connector</module>
</modules>
<dependencies>
diff --git a/pom.xml b/pom.xml
index ba4e6dd..0d6fe4d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1948,6 +1948,19 @@
<version>6.9.6</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <version>3.21.0</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <version>4.1.1</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.hamcrest</groupId>