Skip to content

Commit d9f4ce2

Browse files
zUniQueXjansupol
authored andcommitted
Issue #4881 - Java client connector
Provide a basic implementation of a client connector using java.net.http.HttpClient Signed-off-by: Steffen Nießing <[email protected]>
1 parent 558a145 commit d9f4ce2

File tree

15 files changed

+884
-0
lines changed

15 files changed

+884
-0
lines changed

bom/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@
7373
<artifactId>jersey-grizzly-connector</artifactId>
7474
<version>${project.version}</version>
7575
</dependency>
76+
<dependency>
77+
<groupId>org.glassfish.jersey.connectors</groupId>
78+
<artifactId>jersey-java-connector</artifactId>
79+
<version>${project.version}</version>
80+
</dependency>
7681
<dependency>
7782
<groupId>org.glassfish.jersey.connectors</groupId>
7883
<artifactId>jersey-jetty-connector</artifactId>

connectors/java-connector/pom.xml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
5+
6+
This program and the accompanying materials are made available under the
7+
terms of the Eclipse Public License v. 2.0, which is available at
8+
http://www.eclipse.org/legal/epl-2.0.
9+
10+
This Source Code may also be made available under the following Secondary
11+
Licenses when the conditions for such availability set forth in the
12+
Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
13+
version 2 with the GNU Classpath Exception, which is available at
14+
https://www.gnu.org/software/classpath/license.html.
15+
16+
SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
17+
18+
-->
19+
20+
<project xmlns="http://maven.apache.org/POM/4.0.0"
21+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
22+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
23+
<parent>
24+
<artifactId>project</artifactId>
25+
<groupId>org.glassfish.jersey.connectors</groupId>
26+
<version>3.1.0-SNAPSHOT</version>
27+
</parent>
28+
<modelVersion>4.0.0</modelVersion>
29+
30+
<artifactId>jersey-java-connector</artifactId>
31+
<packaging>jar</packaging>
32+
<name>jersey-connectors-java</name>
33+
34+
<description>Jersey Client Transport via Java's HttpClient</description>
35+
36+
<properties>
37+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
38+
</properties>
39+
40+
<dependencies>
41+
<dependency>
42+
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
43+
<artifactId>jersey-test-framework-provider-bundle</artifactId>
44+
<version>${project.version}</version>
45+
<type>pom</type>
46+
<scope>test</scope>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.assertj</groupId>
50+
<artifactId>assertj-core</artifactId>
51+
<scope>test</scope>
52+
</dependency>
53+
<dependency>
54+
<groupId>org.awaitility</groupId>
55+
<artifactId>awaitility</artifactId>
56+
<scope>test</scope>
57+
</dependency>
58+
</dependencies>
59+
60+
<build>
61+
<plugins>
62+
<plugin>
63+
<groupId>com.sun.istack</groupId>
64+
<artifactId>istack-commons-maven-plugin</artifactId>
65+
<inherited>true</inherited>
66+
</plugin>
67+
<plugin>
68+
<groupId>org.codehaus.mojo</groupId>
69+
<artifactId>build-helper-maven-plugin</artifactId>
70+
<inherited>true</inherited>
71+
</plugin>
72+
<plugin>
73+
<groupId>org.apache.maven.plugins</groupId>
74+
<artifactId>maven-compiler-plugin</artifactId>
75+
</plugin>
76+
</plugins>
77+
</build>
78+
79+
</project>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.java.connector;
18+
19+
import org.glassfish.jersey.internal.util.PropertiesClass;
20+
21+
import java.net.http.HttpClient;
22+
23+
/**
24+
* Provides configuration properties for a {@link JavaConnector}.
25+
*
26+
* @author Steffen Nießing
27+
*/
28+
@PropertiesClass
29+
public class JavaClientProperties {
30+
/**
31+
* Configuration of the {@link java.net.CookieHandler} that should be used by the {@link HttpClient}.
32+
* If this option is not set, {@link HttpClient#cookieHandler()} will return an empty {@link java.util.Optional}
33+
* and therefore no cookie handler will be used.
34+
*
35+
* A provided value to this option has to be of type {@link java.net.CookieHandler}.
36+
*/
37+
public static final String COOKIE_HANDLER = "jersey.config.java.client.cookieHandler";
38+
39+
/**
40+
* Configuration of SSL parameters used by the {@link HttpClient}.
41+
* If this option is not set, then the {@link HttpClient} will use <it>implementation specific</it> default values.
42+
*
43+
* A provided value to this option has to be of type {@link javax.net.ssl.SSLParameters}.
44+
*/
45+
public static final String SSL_PARAMETERS = "jersey.config.java.client.sslParameters";
46+
47+
/**
48+
* Prevent this class from instantiation.
49+
*/
50+
private JavaClientProperties() {}
51+
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
* Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.java.connector;
18+
19+
import jakarta.ws.rs.ProcessingException;
20+
import jakarta.ws.rs.client.Client;
21+
import jakarta.ws.rs.core.Configuration;
22+
import jakarta.ws.rs.core.MultivaluedMap;
23+
import org.glassfish.jersey.client.ClientProperties;
24+
import org.glassfish.jersey.client.ClientRequest;
25+
import org.glassfish.jersey.client.ClientResponse;
26+
import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
27+
import org.glassfish.jersey.client.spi.Connector;
28+
import org.glassfish.jersey.internal.Version;
29+
import org.glassfish.jersey.message.internal.OutboundMessageContext;
30+
import org.glassfish.jersey.message.internal.Statuses;
31+
32+
import javax.net.ssl.SSLContext;
33+
import javax.net.ssl.SSLParameters;
34+
import java.io.ByteArrayOutputStream;
35+
import java.io.IOException;
36+
import java.io.InputStream;
37+
import java.io.OutputStream;
38+
import java.net.CookieHandler;
39+
import java.net.http.HttpClient;
40+
import java.net.http.HttpRequest;
41+
import java.net.http.HttpResponse;
42+
import java.time.Duration;
43+
import java.time.temporal.ChronoUnit;
44+
import java.util.List;
45+
import java.util.Map;
46+
import java.util.concurrent.CompletableFuture;
47+
import java.util.concurrent.Future;
48+
import java.util.logging.Logger;
49+
50+
/**
51+
* Provides a Jersey client {@link Connector}, which internally uses Java's {@link HttpClient}.
52+
* The following properties are provided to Java's {@link HttpClient.Builder} during creation of the {@link HttpClient}:
53+
* <ul>
54+
* <li>{@link ClientProperties#CONNECT_TIMEOUT}</li>
55+
* <li>{@link ClientProperties#FOLLOW_REDIRECTS}</li>
56+
* <li>{@link JavaClientProperties#COOKIE_HANDLER}</li>
57+
* <li>{@link JavaClientProperties#SSL_PARAMETERS}</li>
58+
* </ul>
59+
*
60+
* @author Steffen Nießing
61+
*/
62+
public class JavaConnector implements Connector {
63+
private static final Logger LOGGER = Logger.getLogger(JavaConnector.class.getName());
64+
65+
private final HttpClient httpClient;
66+
67+
/**
68+
* Constructs a new {@link Connector} for a Jersey client instance using Java's {@link HttpClient}.
69+
*
70+
* @param client a Jersey client instance to get additional configuration properties from (e.g. {@link SSLContext})
71+
* @param configuration the configuration properties for this connector
72+
*/
73+
public JavaConnector(final Client client, final Configuration configuration) {
74+
HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
75+
httpClientBuilder.version(HttpClient.Version.HTTP_1_1);
76+
SSLContext sslContext = client.getSslContext();
77+
if (sslContext != null) {
78+
httpClientBuilder.sslContext(sslContext);
79+
}
80+
Integer connectTimeout = getPropertyOrNull(configuration, ClientProperties.CONNECT_TIMEOUT, Integer.class);
81+
if (connectTimeout != null) {
82+
httpClientBuilder.connectTimeout(Duration.of(connectTimeout, ChronoUnit.MILLIS));
83+
}
84+
CookieHandler cookieHandler = getPropertyOrNull(configuration, JavaClientProperties.COOKIE_HANDLER, CookieHandler.class);
85+
if (cookieHandler != null) {
86+
httpClientBuilder.cookieHandler(cookieHandler);
87+
}
88+
Boolean redirect = getPropertyOrNull(configuration, ClientProperties.FOLLOW_REDIRECTS, Boolean.class);
89+
if (redirect != null) {
90+
httpClientBuilder.followRedirects(redirect ? HttpClient.Redirect.ALWAYS : HttpClient.Redirect.NEVER);
91+
} else {
92+
httpClientBuilder.followRedirects(HttpClient.Redirect.NORMAL);
93+
}
94+
SSLParameters sslParameters = getPropertyOrNull(configuration, JavaClientProperties.SSL_PARAMETERS, SSLParameters.class);
95+
if (sslParameters != null) {
96+
httpClientBuilder.sslParameters(sslParameters);
97+
}
98+
this.httpClient = httpClientBuilder.build();
99+
}
100+
101+
/**
102+
* Implements a {@link org.glassfish.jersey.message.internal.OutboundMessageContext.StreamProvider}
103+
* for a {@link ByteArrayOutputStream}.
104+
*/
105+
private static class ByteArrayOutputStreamProvider implements OutboundMessageContext.StreamProvider {
106+
private ByteArrayOutputStream byteArrayOutputStream;
107+
108+
public ByteArrayOutputStream getByteArrayOutputStream() {
109+
return byteArrayOutputStream;
110+
}
111+
112+
@Override
113+
public OutputStream getOutputStream(int contentLength) throws IOException {
114+
return this.byteArrayOutputStream = new ByteArrayOutputStream(contentLength);
115+
}
116+
}
117+
118+
/**
119+
* Builds a request for the {@link HttpClient} from Jersey's {@link ClientRequest}.
120+
*
121+
* @param request the Jersey request to get request data from
122+
* @return the {@link HttpRequest} instance for the {@link HttpClient} request
123+
*/
124+
private HttpRequest getHttpRequest(ClientRequest request) {
125+
HttpRequest.Builder builder = HttpRequest.newBuilder();
126+
builder.uri(request.getUri());
127+
HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.noBody();
128+
if (request.hasEntity()) {
129+
try {
130+
request.enableBuffering();
131+
ByteArrayOutputStreamProvider byteBufferStreamProvider = new ByteArrayOutputStreamProvider();
132+
request.setStreamProvider(byteBufferStreamProvider);
133+
request.writeEntity();
134+
bodyPublisher = HttpRequest.BodyPublishers.ofByteArray(
135+
byteBufferStreamProvider.getByteArrayOutputStream().toByteArray()
136+
);
137+
} catch (IOException e) {
138+
throw new ProcessingException(LocalizationMessages.ERROR_INVALID_ENTITY(), e);
139+
}
140+
}
141+
builder.method(request.getMethod(), bodyPublisher);
142+
for (Map.Entry<String, List<String>> entry : request.getRequestHeaders().entrySet()) {
143+
String headerName = entry.getKey();
144+
for (String headerValue : entry.getValue()) {
145+
builder.header(headerName, headerValue);
146+
}
147+
}
148+
return builder.build();
149+
}
150+
151+
/**
152+
* Retrieves a property from the configuration, if it was provided.
153+
*
154+
* @param configuration the {@link Configuration} to get the property information from
155+
* @param propertyKey the name of the property to retrieve
156+
* @param resultClass the type to which the property value should be case
157+
* @param <T> the generic type parameter of the result type
158+
* @return the requested property or {@code null}, if it was not provided or has the wrong type
159+
*/
160+
@SuppressWarnings("unchecked")
161+
private <T> T getPropertyOrNull(final Configuration configuration, final String propertyKey, final Class<T> resultClass) {
162+
Object propertyObject = configuration.getProperty(propertyKey);
163+
if (propertyObject == null) {
164+
return null;
165+
}
166+
if (!resultClass.isInstance(propertyObject)) {
167+
LOGGER.warning(LocalizationMessages.ERROR_INVALID_CLASS(propertyKey, resultClass.getName()));
168+
return null;
169+
}
170+
return (T) propertyObject;
171+
}
172+
173+
/**
174+
* Translates a {@link HttpResponse} from the {@link HttpClient} to a Jersey {@link ClientResponse}.
175+
*
176+
* @param request the {@link ClientRequest} to get additional information (e.g. header values) from
177+
* @param response the {@link HttpClient} response object
178+
* @return the translated Jersey {@link ClientResponse} object
179+
*/
180+
private ClientResponse buildClientResponse(ClientRequest request, HttpResponse<InputStream> response) {
181+
ClientResponse clientResponse = new ClientResponse(Statuses.from(response.statusCode()), request);
182+
MultivaluedMap<String, String> headers = clientResponse.getHeaders();
183+
for (Map.Entry<String, List<String>> entry : response.headers().map().entrySet()) {
184+
String headerName = entry.getKey();
185+
if (headers.get(headerName) != null) {
186+
headers.get(headerName).addAll(entry.getValue());
187+
} else {
188+
headers.put(headerName, entry.getValue());
189+
}
190+
}
191+
clientResponse.setEntityStream(response.body());
192+
return clientResponse;
193+
}
194+
195+
/**
196+
* Returns the underlying {@link HttpClient} instance used by this connector.
197+
*
198+
* @return the Java {@link HttpClient} instance
199+
*/
200+
public HttpClient getHttpClient() {
201+
return httpClient;
202+
}
203+
204+
@Override
205+
public ClientResponse apply(ClientRequest request) {
206+
HttpRequest httpRequest = getHttpRequest(request);
207+
try {
208+
HttpResponse<InputStream> response = this.httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofInputStream());
209+
return buildClientResponse(request, response);
210+
} catch (IOException | InterruptedException e) {
211+
throw new ProcessingException(e);
212+
}
213+
}
214+
215+
@Override
216+
public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) {
217+
HttpRequest httpRequest = getHttpRequest(request);
218+
CompletableFuture<ClientResponse> response = this.httpClient
219+
.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofInputStream())
220+
.thenApply(httpResponse -> buildClientResponse(request, httpResponse));
221+
response.thenAccept(callback::response);
222+
return response;
223+
}
224+
225+
@Override
226+
public String getName() {
227+
return "Java HttpClient Connector " + Version.getVersion();
228+
}
229+
230+
@Override
231+
public void close() {
232+
233+
}
234+
}

0 commit comments

Comments
 (0)