Skip to content

Commit 9b012df

Browse files
authored
Add a JDK 11 HTTP client (#10936)
This creates a new Java 11+ only library that uses Java's own built-in HTTP and WebSocket clients.
1 parent 9656da7 commit 9b012df

12 files changed

Lines changed: 506 additions & 3 deletions

File tree

java/src/org/openqa/selenium/grid/server/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ java_library(
66
srcs = glob(["*.java"]),
77
visibility = [
88
"//java/src/org/openqa/selenium:__subpackages__",
9+
"//java/test/org/openqa/selenium:__subpackages__",
910
"//java/test/org/openqa/selenium/environment:__pkg__",
10-
"//java/test/org/openqa/selenium/grid:__subpackages__",
1111
],
1212
runtime_deps = [
1313
"//java/src/org/openqa/selenium/events/zeromq",

java/src/org/openqa/selenium/remote/http/ClientConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ public URL baseUrl() {
9494
}
9595
}
9696

97+
public ClientConfig connectionTimeout(Duration timeout) {
98+
return new ClientConfig(
99+
baseUri,
100+
Require.nonNull("Connection timeout", timeout),
101+
readTimeout,
102+
filters,
103+
proxy,
104+
credentials);
105+
}
106+
97107
public Duration connectionTimeout() {
98108
return connectionTimeout;
99109
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
load("//java:defs.bzl", "java_export")
2+
load("//java:version.bzl", "SE_VERSION")
3+
4+
java_export(
5+
name = "jdk",
6+
srcs = glob(["*.java"]),
7+
javacopts = [
8+
"--release",
9+
"11",
10+
],
11+
maven_coordinates = "org.seleniumhq.selenium:selenium-http-jdk-client:%s" % SE_VERSION,
12+
pom_template = "//java/src/org/openqa/selenium:template-pom",
13+
visibility = [
14+
"//visibility:public",
15+
],
16+
deps = [
17+
"//java:auto-service",
18+
"//java/src/org/openqa/selenium:core",
19+
"//java/src/org/openqa/selenium/remote/http",
20+
],
21+
)
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package org.openqa.selenium.remote.http.jdk;
2+
3+
import com.google.auto.service.AutoService;
4+
import org.openqa.selenium.Credentials;
5+
import org.openqa.selenium.TimeoutException;
6+
import org.openqa.selenium.UsernameAndPassword;
7+
import org.openqa.selenium.remote.http.BinaryMessage;
8+
import org.openqa.selenium.remote.http.ClientConfig;
9+
import org.openqa.selenium.remote.http.CloseMessage;
10+
import org.openqa.selenium.remote.http.HttpClient;
11+
import org.openqa.selenium.remote.http.HttpClientName;
12+
import org.openqa.selenium.remote.http.HttpRequest;
13+
import org.openqa.selenium.remote.http.HttpResponse;
14+
import org.openqa.selenium.remote.http.Message;
15+
import org.openqa.selenium.remote.http.TextMessage;
16+
import org.openqa.selenium.remote.http.WebSocket;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.io.UncheckedIOException;
21+
import java.net.Authenticator;
22+
import java.net.PasswordAuthentication;
23+
import java.net.Proxy;
24+
import java.net.ProxySelector;
25+
import java.net.SocketAddress;
26+
import java.net.URI;
27+
import java.net.URISyntaxException;
28+
import java.net.http.HttpResponse.BodyHandler;
29+
import java.net.http.HttpResponse.BodyHandlers;
30+
import java.net.http.HttpTimeoutException;
31+
import java.nio.ByteBuffer;
32+
import java.util.List;
33+
import java.util.Objects;
34+
import java.util.concurrent.CompletableFuture;
35+
import java.util.concurrent.CompletionStage;
36+
37+
import static java.net.http.HttpClient.Redirect.ALWAYS;
38+
39+
public class JdkHttpClient implements HttpClient {
40+
private final JdkHttpMessages messages;
41+
private final java.net.http.HttpClient client;
42+
43+
JdkHttpClient(ClientConfig config) {
44+
Objects.requireNonNull(config, "Client config must be set");
45+
46+
this.messages = new JdkHttpMessages(config);
47+
48+
java.net.http.HttpClient.Builder builder = java.net.http.HttpClient.newBuilder()
49+
.connectTimeout(config.connectionTimeout())
50+
.followRedirects(ALWAYS);
51+
52+
53+
Credentials credentials = config.credentials();
54+
if (credentials != null) {
55+
if (!(credentials instanceof UsernameAndPassword)) {
56+
throw new IllegalArgumentException("Credentials must be a user name and password: " + credentials);
57+
}
58+
UsernameAndPassword uap = (UsernameAndPassword) credentials;
59+
Authenticator authenticator = new Authenticator() {
60+
@Override
61+
protected PasswordAuthentication getPasswordAuthentication() {
62+
return new PasswordAuthentication(uap.username(), uap.password().toCharArray());
63+
}
64+
};
65+
builder = builder.authenticator(authenticator);
66+
}
67+
68+
Proxy proxy = config.proxy();
69+
if (proxy != null) {
70+
ProxySelector proxySelector = new ProxySelector() {
71+
@Override
72+
public List<Proxy> select(URI uri) {
73+
if (proxy == null) {
74+
return List.of();
75+
}
76+
if (uri.getScheme().toLowerCase().startsWith("http")) {
77+
return List.of(proxy);
78+
}
79+
return List.of();
80+
}
81+
82+
@Override
83+
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
84+
// Do nothing
85+
}
86+
};
87+
builder = builder.proxy(proxySelector);
88+
}
89+
90+
this.client = builder.build();
91+
}
92+
93+
@Override
94+
public WebSocket openSocket(HttpRequest request, WebSocket.Listener listener) {
95+
URI uri = getWebSocketUri(request);
96+
97+
CompletableFuture<java.net.http.WebSocket> webSocketCompletableFuture =
98+
client.newWebSocketBuilder().buildAsync(
99+
uri,
100+
new java.net.http.WebSocket.Listener() {
101+
102+
@Override
103+
public CompletionStage<?> onText(java.net.http.WebSocket webSocket, CharSequence data, boolean last) {
104+
listener.onText(data);
105+
return null;
106+
}
107+
108+
@Override
109+
public CompletionStage<?> onBinary(java.net.http.WebSocket webSocket, ByteBuffer data, boolean last) {
110+
byte[] ary = new byte[data.remaining()];
111+
data.get(ary, 0, ary.length);
112+
113+
listener.onBinary(ary);
114+
return null;
115+
}
116+
117+
@Override
118+
public CompletionStage<?> onClose(java.net.http.WebSocket webSocket, int statusCode, String reason) {
119+
listener.onClose(statusCode, reason);
120+
return null;
121+
}
122+
123+
@Override
124+
public void onError(java.net.http.WebSocket webSocket, Throwable error) {
125+
listener.onError(error);
126+
}
127+
});
128+
129+
java.net.http.WebSocket underlyingSocket = webSocketCompletableFuture.join();
130+
131+
return new WebSocket() {
132+
@Override
133+
public WebSocket send(Message message) {
134+
if (message instanceof BinaryMessage) {
135+
BinaryMessage binaryMessage = (BinaryMessage) message;
136+
underlyingSocket.sendBinary(ByteBuffer.wrap(binaryMessage.data()), true);
137+
} else if (message instanceof TextMessage) {
138+
TextMessage textMessage = (TextMessage) message;
139+
underlyingSocket.sendText(textMessage.text(), true);
140+
} else if (message instanceof CloseMessage) {
141+
CloseMessage closeMessage = (CloseMessage) message;
142+
underlyingSocket.sendClose(closeMessage.code(), closeMessage.reason());
143+
} else {
144+
throw new IllegalArgumentException("Unsupport message type: " + message);
145+
}
146+
return this;
147+
}
148+
149+
@Override
150+
public void close() {
151+
underlyingSocket.sendClose(1000, "WebDriver closing socket");
152+
}
153+
};
154+
}
155+
156+
private URI getWebSocketUri(HttpRequest request) {
157+
URI uri = messages.createRequest(request).uri();
158+
if ("http".equalsIgnoreCase(uri.getScheme())) {
159+
try {
160+
uri = new URI("ws", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
161+
} catch (URISyntaxException e) {
162+
throw new RuntimeException(e);
163+
}
164+
} else if ("https".equalsIgnoreCase(uri.getScheme())) {
165+
try {
166+
uri = new URI("wss", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
167+
} catch (URISyntaxException e) {
168+
throw new RuntimeException(e);
169+
}
170+
}
171+
return uri;
172+
}
173+
174+
@Override
175+
public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
176+
Objects.requireNonNull(req, "Request");
177+
BodyHandler<InputStream> streamHandler = BodyHandlers.ofInputStream();
178+
try {
179+
return messages.createResponse(client.send(messages.createRequest(req), streamHandler));
180+
} catch (HttpTimeoutException e) {
181+
throw new TimeoutException(e);
182+
} catch (IOException e) {
183+
throw new UncheckedIOException(e);
184+
} catch (InterruptedException e) {
185+
Thread.currentThread().interrupt();
186+
throw new RuntimeException(e);
187+
}
188+
}
189+
190+
@AutoService(HttpClient.Factory.class)
191+
@HttpClientName("jdk-http-client")
192+
public static class Factory implements HttpClient.Factory {
193+
194+
@Override
195+
public HttpClient createClient(ClientConfig config) {
196+
Objects.requireNonNull(config, "Client config must be set");
197+
return new JdkHttpClient(config);
198+
}
199+
}
200+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package org.openqa.selenium.remote.http.jdk;
2+
3+
import org.openqa.selenium.remote.http.AddSeleniumUserAgent;
4+
import org.openqa.selenium.remote.http.ClientConfig;
5+
import org.openqa.selenium.remote.http.HttpRequest;
6+
import org.openqa.selenium.remote.http.HttpResponse;
7+
8+
import java.io.InputStream;
9+
import java.net.URI;
10+
import java.net.URLEncoder;
11+
import java.net.http.HttpRequest.BodyPublishers;
12+
import java.util.Objects;
13+
import java.util.stream.Collectors;
14+
import java.util.stream.StreamSupport;
15+
16+
import static java.nio.charset.StandardCharsets.UTF_8;
17+
18+
class JdkHttpMessages {
19+
20+
private final ClientConfig config;
21+
22+
public JdkHttpMessages(ClientConfig config) {
23+
this.config = Objects.requireNonNull(config, "Client config");
24+
}
25+
26+
public java.net.http.HttpRequest createRequest(HttpRequest req) {
27+
String rawUrl = getRawUrl(config.baseUri(), req.getUri());
28+
29+
// Add query string if necessary
30+
String queryString = StreamSupport.stream(req.getQueryParameterNames().spliterator(), false)
31+
.map(name -> {
32+
return StreamSupport.stream(req.getQueryParameters(name).spliterator(), false)
33+
.map(value -> String.format("%s=%s", URLEncoder.encode(name, UTF_8), URLEncoder.encode(value, UTF_8)))
34+
.collect(Collectors.joining("&"));
35+
})
36+
.collect(Collectors.joining("&"));
37+
38+
if (!queryString.isEmpty()) {
39+
rawUrl = rawUrl + "?" + queryString;
40+
}
41+
42+
java.net.http.HttpRequest.Builder builder = java.net.http.HttpRequest.newBuilder().uri(URI.create(rawUrl));
43+
44+
switch (req.getMethod()) {
45+
case DELETE:
46+
builder = builder.DELETE();
47+
break;
48+
49+
case GET:
50+
builder = builder.GET();
51+
break;
52+
53+
case POST:
54+
builder = builder.POST(BodyPublishers.ofInputStream(req.getContent()));
55+
break;
56+
57+
case PUT:
58+
builder = builder.PUT(BodyPublishers.ofInputStream(req.getContent()));
59+
break;
60+
61+
default:
62+
throw new IllegalArgumentException(String.format("Unsupported request method %s: %s", req.getMethod(), req));
63+
}
64+
65+
for (String name : req.getHeaderNames()) {
66+
for (String value : req.getHeaders(name)) {
67+
builder = builder.header(name, value);
68+
}
69+
}
70+
71+
if (req.getHeader("User-Agent") == null) {
72+
builder = builder.header("User-Agent", AddSeleniumUserAgent.USER_AGENT);
73+
}
74+
75+
builder.timeout(config.readTimeout());
76+
77+
return builder.build();
78+
}
79+
80+
private String getRawUrl(URI baseUrl, String uri) {
81+
String rawUrl;
82+
if (uri.startsWith("ws://") || uri.startsWith("wss://") ||
83+
uri.startsWith("http://") || uri.startsWith("https://")) {
84+
rawUrl = uri;
85+
} else {
86+
rawUrl = baseUrl.toString().replaceAll("/$", "") + uri;
87+
}
88+
89+
return rawUrl;
90+
}
91+
92+
public HttpResponse createResponse(java.net.http.HttpResponse<InputStream> response) {
93+
HttpResponse res = new HttpResponse();
94+
res.setStatus(response.statusCode());
95+
response.headers().map()
96+
.forEach((name, values) -> values.stream().filter(Objects::nonNull).forEach(value -> res.addHeader(name, value)));
97+
res.setContent(response::body);
98+
99+
return res;
100+
}
101+
}

java/src/org/openqa/selenium/remote/http/netty/NettyMessages.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ private NettyMessages() {
4747
// Utility classes.
4848
}
4949

50-
protected static Request toNettyRequest(ClientConfig config,
51-
HttpRequest request) {
50+
protected static Request toNettyRequest(ClientConfig config, HttpRequest request) {
5251

5352
URI baseUrl = config.baseUri();
5453
int timeout = toClampedInt(config.readTimeout().toMillis());
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
load("//java:defs.bzl", "JUNIT5_DEPS", "java_test_suite")
2+
3+
java_test_suite(
4+
name = "medium-tests",
5+
size = "medium",
6+
srcs = glob(["*.java"]),
7+
deps = [
8+
"//java/src/org/openqa/selenium/remote/http",
9+
"//java/src/org/openqa/selenium/remote/http/jdk",
10+
"//java/test/org/openqa/selenium/remote/internal:test-lib",
11+
] + JUNIT5_DEPS,
12+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.openqa.selenium.remote.http.jdk;
2+
3+
import org.openqa.selenium.remote.http.HttpClient;
4+
import org.openqa.selenium.remote.internal.HttpClientTestBase;
5+
6+
public class JdkHttpClientTest extends HttpClientTestBase {
7+
8+
@Override
9+
protected HttpClient.Factory createFactory() {
10+
return new JdkHttpClient.Factory();
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.openqa.selenium.remote.http.jdk;
2+
3+
import org.openqa.selenium.remote.http.HttpClient;
4+
import org.openqa.selenium.remote.internal.WebSocketTestBase;
5+
6+
public class JdkWebSocketTest extends WebSocketTestBase {
7+
@Override
8+
protected HttpClient.Factory createFactory() {
9+
return new JdkHttpClient.Factory();
10+
}
11+
}

0 commit comments

Comments
 (0)