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