org.openqa.selenium.devtools.Connection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of selenium-remote-driver Show documentation
Show all versions of selenium-remote-driver Show documentation
Selenium automates browsers. That's it! What you do with that power is entirely up to you.
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.openqa.selenium.devtools;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.openqa.selenium.json.Json.MAP_TYPE;
import static org.openqa.selenium.remote.http.HttpMethod.GET;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import org.openqa.selenium.devtools.target.model.SessionId;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonInput;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.WebSocket;
import java.io.Closeable;
import java.io.StringReader;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
public class Connection implements Closeable {
private static final Json JSON = new Json();
private static final AtomicLong NEXT_ID = new AtomicLong(1L);
private final WebSocket socket;
private final Map> methodCallbacks = new LinkedHashMap<>();
private final Multimap, Consumer> eventCallbacks = HashMultimap.create();
public Connection(HttpClient client, String url) {
Objects.requireNonNull(client, "HTTP client must be set.");
Objects.requireNonNull(url, "URL to connect to must be set.");
socket = client.openSocket(new HttpRequest(GET, url), new Listener());
}
public CompletableFuture send(SessionId sessionId, Command command) {
long id = NEXT_ID.getAndIncrement();
CompletableFuture result = new CompletableFuture<>();
methodCallbacks.put(id, input -> {
X value = command.getMapper().apply(input);
result.complete(value);
});
ImmutableMap.Builder serialized = ImmutableMap.builder();
serialized.put("id", id);
serialized.put("method", command.getMethod());
serialized.put("params", command.getParams());
if (sessionId != null) {
serialized.put("sessionId", sessionId);
}
socket.sendText(JSON.toJson(serialized.build()));
return result;
}
public X sendAndWait(SessionId sessionId, Command command, Duration timeout) {
try {
return send(sessionId, command).get(timeout.toMillis(), MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Thread has been interrupted", e);
} catch (ExecutionException e) {
Throwable cause = e;
if (e.getCause() != null) {
cause = e.getCause();
}
throw new DevToolsException(cause);
} catch (TimeoutException e) {
throw new org.openqa.selenium.TimeoutException(e);
}
}
public void addListener(Event event, Consumer handler) {
Objects.requireNonNull(event);
Objects.requireNonNull(handler);
eventCallbacks.put(event, handler);
}
@Override
public void close() {
socket.close();
}
private class Listener extends WebSocket.Listener {
@Override
public void onText(CharSequence data) {
// It's kind of gross to decode the data twice, but this lets us get started on something
// that feels nice to users.
// TODO: decode once, and once only
String asString = String.valueOf(data);
Map raw = JSON.toType(asString, MAP_TYPE);
if (raw.get("id") instanceof Number && raw.get("result") != null) {
Consumer consumer = methodCallbacks.remove(((Number) raw.get("id")).longValue());
if (consumer == null) {
return;
}
try (StringReader reader = new StringReader(asString);
JsonInput input = JSON.newInput(reader)) {
input.beginObject();
while (input.hasNext()) {
switch (input.nextName()) {
case "result":
consumer.accept(input);
break;
default:
input.skipValue();
}
}
input.endObject();
}
} else if (raw.get("method") instanceof String && raw.get("params") instanceof Map) {
System.out.println("Seen: " + raw);
// TODO: Also only decode once.
eventCallbacks.keySet().stream()
.filter(event -> raw.get("method").equals(event.getMethod()))
.forEach(event -> {
// TODO: This is grossly inefficient. I apologise, and we should fix this.
try (StringReader reader = new StringReader(asString);
JsonInput input = JSON.newInput(reader)) {
Object value = null;
input.beginObject();
while (input.hasNext()) {
switch (input.nextName()) {
case "params":
value = event.getMapper().apply(input);
break;
default:
input.skipValue();
break;
}
}
input.endObject();
if (value == null) {
// Do nothing.
return;
}
final Object finalValue = value;
for (Consumer action : eventCallbacks.get(event)) {
@SuppressWarnings("unchecked") Consumer