Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions java/src/org/openqa/selenium/bidi/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.Beta;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.JsonInput;
import org.openqa.selenium.json.Json;

@Beta
public class Command<X> {

private static final Json JSON = new Json();

private final String method;
private final Map<String, @Nullable Object> params;
private final Function<JsonInput, X> mapper;
private final Function<@Nullable Object, X> mapper;
private final boolean sendsResponse;

public Command(String method, Map<String, @Nullable Object> params) {
Expand All @@ -42,18 +44,23 @@ public Command(String method, Map<String, @Nullable Object> params) {

public Command(String method, Map<String, @Nullable Object> params, Type typeOfX) {
this(
method, params, input -> input.readNonNull(Require.nonNull("Type to convert to", typeOfX)));
method,
params,
result ->
Require.nonNull(
"Command result",
JSON.convert(result, Require.nonNull("Type to convert to", typeOfX))));
}

public Command(
String method, Map<String, @Nullable Object> params, Function<JsonInput, X> mapper) {
String method, Map<String, @Nullable Object> params, Function<@Nullable Object, X> mapper) {
this(method, params, mapper, true);
}

public Command(
String method,
Map<String, @Nullable Object> params,
Function<JsonInput, X> mapper,
Function<@Nullable Object, X> mapper,
boolean sendsResponse) {
this.method = Require.nonNull("Method name", method);
this.params = unmodifiableMap(new HashMap<>(Require.nonNull("Command parameters", params)));
Expand All @@ -73,7 +80,7 @@ public boolean getSendsResponse() {
return sendsResponse;
}

Function<JsonInput, X> getMapper() {
Function<@Nullable Object, X> getMapper() {
return mapper;
}
}
40 changes: 11 additions & 29 deletions java/src/org/openqa/selenium/bidi/Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import static org.openqa.selenium.remote.http.HttpMethod.GET;

import java.io.Closeable;
import java.io.StringReader;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
Expand All @@ -50,7 +49,6 @@
import org.openqa.selenium.internal.Either;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonInput;
import org.openqa.selenium.json.JsonOutput;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpRequest;
Expand All @@ -69,7 +67,7 @@ public class Connection implements Closeable {
return thread;
});
private static final AtomicLong NEXT_ID = new AtomicLong(1L);
private final Map<Long, Consumer<Either<Throwable, JsonInput>>> methodCallbacks =
private final Map<Long, Consumer<Either<Throwable, @Nullable Object>>> methodCallbacks =
new ConcurrentHashMap<>();
private final ReadWriteLock callbacksLock = new ReentrantReadWriteLock(true);
private final Map<Event<?>, Map<String, Consumer<?>>> eventCallbacks = new HashMap<>();
Expand Down Expand Up @@ -297,50 +295,34 @@ public void onClose(int code, String reason) {
}

private void handle(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

// Parse the incoming message once, here, into a Map. A response then passes its "result"
// value to the command's mapper and an event passes its "params" Map to the event's mapper;
// neither re-parses the message text.
String asString = String.valueOf(data);
LOG.log(getDebugLogLevel(), "<- {0}", asString);

Map<String, Object> raw = JSON.toType(asString, MAP_TYPE);
if (raw.get("id") instanceof Number
&& (raw.get("result") != null || raw.get("error") != null)) {
handleResponse(asString, raw);
handleResponse(raw);
} else if (raw.get("method") instanceof String && raw.get("params") instanceof Map) {
handleEventResponse(raw);
} else {
LOG.warning(() -> "Unhandled type BiDi response type: " + data);
}
}

private void handleResponse(String rawDataString, Map<String, Object> rawDataMap) {
Consumer<Either<Throwable, JsonInput>> consumer =
private void handleResponse(Map<String, Object> rawDataMap) {
Consumer<Either<Throwable, @Nullable Object>> consumer =
methodCallbacks.remove(((Number) rawDataMap.get("id")).longValue());
if (consumer == null) {
return;
}

try (StringReader reader = new StringReader(rawDataString);
JsonInput input = JSON.newInput(reader)) {
input.beginObject();
while (input.hasNext()) {
switch (input.nextName()) {
case "result":
consumer.accept(Either.right(input));
break;

case "error":
consumer.accept(Either.left(new WebDriverException(rawDataString)));
input.skipValue();
break;

default:
input.skipValue();
}
}
input.endObject();
if (rawDataMap.get("error") != null) {
consumer.accept(Either.left(new WebDriverException(JSON.toJson(rawDataMap))));
} else {
consumer.accept(Either.right(rawDataMap.get("result")));
}
}

Expand Down
39 changes: 22 additions & 17 deletions java/src/org/openqa/selenium/bidi/ConverterFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,43 @@
package org.openqa.selenium.bidi;

import java.lang.reflect.Type;
import java.util.Map;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.Beta;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.JsonInput;
import org.openqa.selenium.json.Json;

@Beta
public class ConverterFunctions {

private static final Json JSON = new Json();

private ConverterFunctions() {
throw new IllegalStateException("Utility class");
}

public static <X> Function<JsonInput, @Nullable X> map(final String keyName, Type typeOfX) {
/**
* Build a {@link Command} result mapper for the common case where the useful value is a single
* field of the response's {@code result} object.
*
* <p>The returned function is applied to a command's {@code result} value, which {@link
* org.openqa.selenium.bidi.Connection} has already parsed into a {@code Map<String, Object>}. It
* reads {@code keyName} from that map and deserializes it to {@code typeOfX} via {@link
* Json#convert(Object, Type)}, without re-parsing any JSON text. Both the {@code result} and the
* field are required: a missing {@code result} or a {@code null}/absent field is an error.
*
* @param keyName the field to read from the command's {@code result} object
* @param typeOfX the type to deserialize that field to (class or {@link
* org.openqa.selenium.json.TypeToken})
*/
public static <X> Function<@Nullable Object, X> map(String keyName, Type typeOfX) {
Require.nonNull("Key name", keyName);
Require.nonNull("Type to convert to", typeOfX);

return input -> {
X value = null;

input.beginObject();
while (input.hasNext()) {
String name = input.nextName();
if (keyName.equals(name)) {
value = input.read(typeOfX);
} else {
input.skipValue();
}
}
input.endObject();

return value;
return result -> {
Object value = ((Map<?, ?>) Require.nonNull("Command result", result)).get(keyName);
return Require.nonNull("Field '" + keyName + "'", JSON.convert(value, typeOfX));
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
import org.openqa.selenium.WindowType;
import org.openqa.selenium.bidi.BiDi;
import org.openqa.selenium.bidi.Command;
import org.openqa.selenium.bidi.ConverterFunctions;
import org.openqa.selenium.bidi.HasBiDi;
import org.openqa.selenium.bidi.script.RemoteValue;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonInput;
import org.openqa.selenium.json.TypeToken;
import org.openqa.selenium.print.PrintOptions;

Expand All @@ -51,28 +51,28 @@ public class BrowsingContext {
private static final String RELOAD = "browsingContext.reload";
private static final String HANDLE_USER_PROMPT = "browsingContext.handleUserPrompt";

private static final Function<JsonInput, String> browsingContextIdMapper =
json -> {
return json.readMap().getOrDefault(CONTEXT, "").toString();
};
private static final Function<@Nullable Object, String> browsingContextIdMapper =
result -> asMap(result).getOrDefault(CONTEXT, "").toString();

private static final Function<JsonInput, NavigationResult> navigationInfoMapper =
json -> (NavigationResult) json.readNonNull(NavigationResult.class);
private static final Function<@Nullable Object, NavigationResult> navigationInfoMapper =
result -> Require.nonNull("Navigation result", JSON.convert(result, NavigationResult.class));

private static final Function<JsonInput, List<BrowsingContextInfo>>
private static final Function<@Nullable Object, List<BrowsingContextInfo>>
browsingContextInfoListMapper =
json -> {
result -> {
Type type = new TypeToken<Map<String, List<BrowsingContextInfo>>>() {}.getType();
Map<String, List<BrowsingContextInfo>> result = json.readNonNull(type);
return result.getOrDefault("contexts", emptyList());
Map<String, List<BrowsingContextInfo>> converted =
Require.nonNull("Command result", JSON.convert(result, type));
return converted.getOrDefault("contexts", emptyList());
};

private static final Function<JsonInput, List<RemoteValue>> nodesMapper =
json -> {
Type type = new TypeToken<Map<String, List<RemoteValue>>>() {}.getType();
Map<String, List<RemoteValue>> result = json.readNonNull(type);
return result.get("nodes");
};
private static final Function<@Nullable Object, List<RemoteValue>> nodesMapper =
ConverterFunctions.map("nodes", new TypeToken<List<RemoteValue>>() {}.getType());

@SuppressWarnings("unchecked")
private static Map<String, Object> asMap(@Nullable Object result) {
return (Map<String, Object>) Require.nonNull("Command result", result);
}

public BrowsingContext(WebDriver driver, String id) {
Require.nonNull("WebDriver", driver);
Expand Down Expand Up @@ -234,9 +234,7 @@ public String captureScreenshot() {
new Command<>(
"browsingContext.captureScreenshot",
Map.of(CONTEXT, id),
jsonInput -> {
return (String) jsonInput.readMap().get("data");
}));
ConverterFunctions.map("data", String.class)));
}

public String captureScreenshot(CaptureScreenshotParameters parameters) {
Expand All @@ -248,9 +246,7 @@ public String captureScreenshot(CaptureScreenshotParameters parameters) {
new Command<>(
"browsingContext.captureScreenshot",
params,
jsonInput -> {
return (String) jsonInput.readMap().get("data");
}));
ConverterFunctions.map("data", String.class)));
}

public String captureBoxScreenshot(double x, double y, double width, double height) {
Expand All @@ -267,9 +263,7 @@ public String captureBoxScreenshot(double x, double y, double width, double heig
"y", y,
"width", width,
"height", height)),
jsonInput -> {
return (String) jsonInput.readMap().get("data");
}));
ConverterFunctions.map("data", String.class)));
}

public String captureElementScreenshot(String elementId) {
Expand All @@ -281,9 +275,7 @@ public String captureElementScreenshot(String elementId) {
id,
"clip",
Map.of("type", "element", "element", Map.of("sharedId", elementId))),
jsonInput -> {
return (String) jsonInput.readMap().get("data");
}));
ConverterFunctions.map("data", String.class)));
}

public String captureElementScreenshot(String elementId, String handle) {
Expand All @@ -296,9 +288,7 @@ public String captureElementScreenshot(String elementId, String handle) {
"clip",
Map.of(
"type", "element", "element", Map.of("sharedId", elementId, "handle", handle))),
jsonInput -> {
return (String) jsonInput.readMap().get("data");
}));
ConverterFunctions.map("data", String.class)));
}

public void setViewport(int width, int height) {
Expand Down Expand Up @@ -381,9 +371,7 @@ public String print(PrintOptions printOptions) {
new Command<>(
"browsingContext.print",
printOptionsParams,
jsonInput -> {
return (String) jsonInput.readMap().get("data");
}));
ConverterFunctions.map("data", String.class)));
}

public void traverseHistory(long delta) {
Expand Down
32 changes: 19 additions & 13 deletions java/src/org/openqa/selenium/bidi/module/Browser.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,39 +21,45 @@
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.Beta;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.bidi.BiDi;
import org.openqa.selenium.bidi.Command;
import org.openqa.selenium.bidi.ConverterFunctions;
import org.openqa.selenium.bidi.HasBiDi;
import org.openqa.selenium.bidi.browser.ClientWindowInfo;
import org.openqa.selenium.bidi.browser.SetDownloadBehaviorParameters;
import org.openqa.selenium.json.JsonInput;
import org.openqa.selenium.internal.Require;

@Beta
public class Browser {
private final BiDi bidi;

private static final Function<JsonInput, String> userContextInfoMapper =
json -> json.readMapElement("userContext");

private static final Function<JsonInput, List<String>> userContextsInfoMapper =
json -> {
List<Map<String, String>> userContexts = json.readMapElement("userContexts");
private static final Function<@Nullable Object, String> userContextInfoMapper =
ConverterFunctions.map("userContext", String.class);

private static final Function<@Nullable Object, List<String>> userContextsInfoMapper =
result -> {
List<Map<String, String>> userContexts = asList(result, "userContexts");
return userContexts.stream()
.map(map -> map.get("userContext"))
.collect(Collectors.toList());
};

private static final Function<JsonInput, List<ClientWindowInfo>> clientWindowsInfoMapper =
json -> {
List<Map<String, Object>> clientWindows = json.readMapElement("clientWindows");
return clientWindows.stream()
.map(map -> ClientWindowInfo.fromJson(map))
.collect(Collectors.toList());
private static final Function<@Nullable Object, List<ClientWindowInfo>> clientWindowsInfoMapper =
result -> {
List<Map<String, Object>> clientWindows = asList(result, "clientWindows");
return clientWindows.stream().map(ClientWindowInfo::fromJson).collect(Collectors.toList());
};

@SuppressWarnings("unchecked")
private static <T> List<T> asList(@Nullable Object result, String key) {
return (List<T>)
Require.nonNull(
"Field " + key, ((Map<?, ?>) Require.nonNull("Command result", result)).get(key));
}

public Browser(WebDriver driver) {
this.bidi = ((HasBiDi) driver).getBiDi();
}
Expand Down
Loading
Loading