Skip to content
Open
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
13 changes: 11 additions & 2 deletions java/src/org/openqa/selenium/grid/graphql/GraphqlHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.openqa.selenium.grid.sessionqueue.NewSessionQueue;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonOutput;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.HttpHandler;
import org.openqa.selenium.remote.http.HttpRequest;
Expand Down Expand Up @@ -179,7 +180,7 @@ public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
response =
new HttpResponse()
.addHeader("Content-Type", JSON_UTF_8)
.setContent(utf8String(JSON.toJson(result.toSpecification())));
.setContent(utf8String(toCompactJson(result.toSpecification())));

HTTP_RESPONSE.accept(span, response);
HTTP_RESPONSE_EVENT.accept(attributeMap, response);
Expand All @@ -191,7 +192,7 @@ public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
response =
new HttpResponse()
.setStatus(HTTP_INTERNAL_ERROR)
.setContent(utf8String(JSON.toJson(result.getErrors())));
.setContent(utf8String(toCompactJson(result.getErrors())));
HTTP_RESPONSE.accept(span, response);
HTTP_RESPONSE_EVENT.accept(attributeMap, response);

Expand Down Expand Up @@ -271,4 +272,12 @@ public int weigh(String key, CompletableFuture<PreparsedDocumentEntry> value) {
return (int) Math.min(boundedWeight, Integer.MAX_VALUE);
}
}

private static String toCompactJson(Object value) {
StringBuilder json = new StringBuilder();
try (JsonOutput out = JSON.newOutput(json)) {
out.setPrettyPrint(false).write(value);
}
return json.toString();
}
}
5 changes: 4 additions & 1 deletion java/src/org/openqa/selenium/json/CollectionCoercer.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,15 @@ public BiFunction<JsonInput, PropertySetting, T> apply(Type type) {
throw new IllegalArgumentException("Unhandled type: " + type.getClass());
}

// Resolve the element coercer once rather than paying a cache lookup per element.
BiFunction<JsonInput, PropertySetting, Object> valueCoercer = coercer.lazyResolve(valueType);

return (jsonInput, setting) -> {
jsonInput.beginArray();
I toReturn = supplier.get();
Consumer<Object> consumer = consumerFactory.apply(toReturn);
while (jsonInput.hasNext()) {
consumer.accept(coercer.coerce(jsonInput, valueType, setting));
consumer.accept(valueCoercer.apply(jsonInput, setting));
}
jsonInput.endArray();

Expand Down
71 changes: 71 additions & 0 deletions java/src/org/openqa/selenium/json/ConstructorCoercer.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ private Map<String, Integer> getParameterIndexes(Parameter[] parameters) {
}

private Object coerceValue(Object value, Type type, PropertySetting setting) {
if (value != null && type instanceof Class) {
Object direct = tryDirectCoercion(value, (Class<?>) type);
if (direct != null) {
return direct;
}
}

StringWriter rawJson = new StringWriter();
try (JsonOutput output = new JsonOutput(rawJson)) {
output.write(value);
Expand All @@ -158,6 +165,70 @@ private Object coerceValue(Object value, Type type, PropertySetting setting) {
}
}

/**
* Coerce the already-parsed value directly when the target is a scalar type, mirroring the
* behavior of the corresponding {@link TypeCoercer}s without serializing the value back to JSON
* text and re-parsing it.
*
* @return the coerced value; {@code null} if the caller must fall back to the JSON round trip
*/
private static Object tryDirectCoercion(Object value, Class<?> target) {
if (target == String.class) {
return (value instanceof String || value instanceof Number || value instanceof Boolean)
? String.valueOf(value)
: null;
}

if ((target == Boolean.class || target == boolean.class) && value instanceof Boolean) {
return value;
}

if (value instanceof Number) {
Number number = (Number) value;
if (target == Integer.class || target == int.class) {
return number.intValue();
}
if (target == Long.class || target == long.class) {
return number.longValue();
}
if (target == Double.class || target == double.class) {
return number.doubleValue();
}
if (target == Float.class || target == float.class) {
return number.floatValue();
}
if (target == Short.class || target == short.class) {
return number.shortValue();
}
if (target == Byte.class || target == byte.class) {
return number.byteValue();
}
if (target == Number.class) {
if (number instanceof Long) {
return number;
}
double doubleValue = number.doubleValue();
if (doubleValue % 1 != 0 || doubleValue < Long.MIN_VALUE || doubleValue > Long.MAX_VALUE) {
return doubleValue;
}
return number.longValue();
}
return null;
}

if (target.isEnum() && value instanceof String) {
for (Object constant : target.getEnumConstants()) {
if (constant.toString().equalsIgnoreCase((String) value)) {
return constant;
}
}
throw new JsonException(
String.format("Unable to find matching enum value for %s in %s", value, target));
}

return null;
}

private class ConstructorCandidate {
private final Constructor<?> constructor;
private final Parameter[] parameters;
Expand Down
147 changes: 145 additions & 2 deletions java/src/org/openqa/selenium/json/Input.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.internal.Require;

/**
Expand All @@ -38,12 +39,12 @@ class Input {
public static final int EOF = -1;

/** the number of chars to buffer */
private static final int BUFFER_SIZE = 4096;
private static final int BUFFER_SIZE = 16384;

/** the number of chars to remember, safe to set to 0 */
private static final int MEMORY_SIZE = 128;

private final Reader source;
private final @Nullable Reader source;

/** a buffer used to minimize read calls and to keep the chars to remember */
private final char[] buffer;
Expand All @@ -66,13 +67,34 @@ public Input(Reader source) {
this.position = -1;
}

/**
* Initialize a new instance of the {@link Input} class that reads directly from a string.
*
* <p>The entire input is buffered up front, sized exactly to the source. This avoids the large
* read buffer and per-chunk {@link Reader} calls of the streaming constructor, which dominate the
* cost of parsing the small payloads typical of WebDriver commands and responses.
*
* @param source string that supplies the input to be processed
*/
public Input(String source) {
Require.nonNull("Source", source);
this.source = null;
this.buffer = source.toCharArray();
this.filled = buffer.length;
this.position = -1;
}

/**
* Extract the next character from the input without consuming it.
*
* @return the next input character as an unsigned UTF-16 code unit (0-65535); {@link #EOF} if
* input is exhausted
*/
public int peek() {
int next = position + 1;
if (next < filled) {
return buffer[next];
}
return fill() ? buffer[position + 1] : EOF;
}

Expand All @@ -83,9 +105,125 @@ public int peek() {
* input is exhausted
*/
public int read() {
int next = position + 1;
if (next < filled) {
position = next;
return buffer[next];
}
return fill() ? buffer[++position] : EOF;
}

/**
* Attempt to consume the body of a JSON string (with the leading quote already consumed) directly
* from the buffered input, including its closing quote.
*
* <p>This is the fast path for strings that contain no escape sequences and whose closing quote
* is already buffered: the result is created straight from the buffer without copying through a
* {@link StringBuilder}. When the string cannot be read this way, nothing is consumed.
*
* @return the string body; {@code null} if the caller must fall back to reading char by char
*/
public @Nullable String readSimpleString() {
if (!fill()) {
return null;
}

int start = position + 1;
for (int i = start; i < filled; i++) {
char c = buffer[i];
if (c == '"') {
position = i;
return new String(buffer, start, i - start);
}
if (c == '\\' || c < 0x20) {
return null;
}
}

return null;
}

/**
* Consume characters that need no special handling within a JSON string, appending them to the
* supplied builder in bulk. Stops before the next '"', '\\', or control character, which is left
* unconsumed.
*
* @param sink {@link StringBuilder} that accumulates the string body
* @return the unconsumed special character as an unsigned UTF-16 code unit; {@link #EOF} if input
* is exhausted
*/
public int appendStringContent(StringBuilder sink) {
while (fill()) {
int start = position + 1;
for (int i = start; i < filled; i++) {
char c = buffer[i];
if (c == '"' || c == '\\' || c < 0x20) {
sink.append(buffer, start, i - start);
position = i - 1;
return c;
}
}
sink.append(buffer, start, filled - start);
position = filled - 1;
}

return EOF;
}

/**
* Consume whitespace characters in bulk, leaving the first non-whitespace character unconsumed.
*/
public void skipWhitespace() {
while (fill()) {
int start = position + 1;
for (int i = start; i < filled; i++) {
if (!isWhitespace(buffer[i])) {
position = i - 1;
return;
}
}
position = filled - 1;
}
}

/**
* Test for whitespace, checking the JSON whitespace characters (RFC 8259 §2) directly before
* consulting {@link Character#isWhitespace}, whose table lookups are measurably slower and which
* is retained only to stay lenient about exotic whitespace between tokens.
*/
private static boolean isWhitespace(char c) {
if (c == ' ' || c == '\n' || c == '\t' || c == '\r') {
return true;
}
return Character.isWhitespace(c);
}

/**
* Consume ASCII digits, appending them to the supplied builder in bulk. Stops before the first
* non-digit character, which is left unconsumed.
*
* @param sink {@link StringBuilder} that accumulates the digits
* @return the unconsumed non-digit character as an unsigned UTF-16 code unit; {@link #EOF} if
* input is exhausted
*/
public int appendDigits(StringBuilder sink) {
while (fill()) {
int start = position + 1;
for (int i = start; i < filled; i++) {
char c = buffer[i];
if (c < '0' || c > '9') {
sink.append(buffer, start, i - start);
position = i - 1;
return c;
}
}
sink.append(buffer, start, filled - start);
position = filled - 1;
}

return EOF;
}

/**
* Return a string containing the most recently consumed input characters.
*
Expand Down Expand Up @@ -129,6 +267,11 @@ public String toString() {
* @throws UncheckedIOException if an I/O exception is encountered
*/
private boolean fill() {
if (source == null) {
// String-backed input is fully buffered on construction.
return filled > position + 1;
}

// do we need to fill the buffer?
while (filled == position + 1) {
try {
Expand Down
Loading
Loading