diff --git a/java/src/org/openqa/selenium/json/Input.java b/java/src/org/openqa/selenium/json/Input.java index 5c2c3dfa642b8..c64b4ebe42f13 100644 --- a/java/src/org/openqa/selenium/json/Input.java +++ b/java/src/org/openqa/selenium/json/Input.java @@ -24,25 +24,31 @@ /** * Similar to a {@link Reader} but with the ability to peek a single character ahead. - * - *

For the sake of providing a useful {@link #toString()} implementation, keeps the most recently + *

+ * For the sake of providing a useful {@link #toString()} implementation, keeps the most recently * read characters in the input buffer. */ class Input { - public static final char EOF = (char) -1; - // the number of chars to buffer + /** end-of-file indicator (0xFFFD) */ + public static final char EOF = (char) -1; // NOTE: Produces Unicode replacement character (0xFFFD) + /** the number of chars to buffer */ private static final int BUFFER_SIZE = 4096; - // the number of chars to remember, safe to set to 0 + /** the number of chars to remember, safe to set to 0 */ private static final int MEMORY_SIZE = 128; private final Reader source; - // a buffer used to minimize read calls and to keep the chars to remember + /** a buffer used to minimize read calls and to keep the chars to remember */ private final char[] buffer; - // the filled area in the buffer + /** the filled area in the buffer */ private int filled; - // the last position read in the buffer + /** the last position read in the buffer */ private int position; + /** + * Initialize a new instance of the {@link Input} class with the specified source. + * + * @param source {@link Reader} object that supplies the input to be processed + */ public Input(Reader source) { this.source = Require.nonNull("Source", source); this.buffer = new char[BUFFER_SIZE + MEMORY_SIZE]; @@ -50,14 +56,29 @@ public Input(Reader source) { this.position = -1; } + /** + * Extract the next character from the input without consuming it. + * + * @return the next input character; {@link #EOF} if input is exhausted + */ public char peek() { return fill() ? buffer[position + 1] : EOF; } + /** + * Read and consume the next character from the input. + * + * @return the next input character; {@link #EOF} if input is exhausted + */ public char read() { return fill() ? buffer[++position] : EOF; } + /** + * Return a string containing the most recently consumed input characters. + * + * @return {@link String} with up to 128 consumed input characters + */ @Override public String toString() { int offset; @@ -74,6 +95,13 @@ public String toString() { return "Last " + length + " characters read: " + new String(buffer, offset, length); } + /** + * If all buffered input has been consumed, read the next chunk into the buffer.
+ * NOTE: The last 128 character of consumed input is retained for debug output. + * + * @return {@code true} if new input is available; {@code false} if input is exhausted + * @throws UncheckedIOException if an I/O exception is encountered + */ private boolean fill() { // do we need to fill the buffer? while (filled == position + 1) { diff --git a/java/src/org/openqa/selenium/json/Json.java b/java/src/org/openqa/selenium/json/Json.java index cc7fc978d2aae..5deb7b31d3bb1 100644 --- a/java/src/org/openqa/selenium/json/Json.java +++ b/java/src/org/openqa/selenium/json/Json.java @@ -27,23 +27,101 @@ import java.util.List; import java.util.Map; +/** + * The Json class is the entrypoint for the JSON processing features of the Selenium API. + * These features include: + *

+ * + * The standard types supported by built-in processing: + * + * + * You can serialize objects for which no explicit coercer has been specified, and the Json API will use a + * generic process to provide best-effort JSON output. For the most predictable results, though, it's best to + * provide a {@code toJson()} method for the Json API to use for serialization. This is especially beneficial + * for objects that contain transient properties that should be omitted from the JSON output. + *

+ * You can deserialize objects for which no explicit handling has been defined. Note that the data type of the + * result will be {@code Map}, which means that you'll need to perform type checking and casting every + * time you extract an entry value from the result. For this reason, it's best to declare a type-specific + * {@code fromJson()} method in every type you need to deserialize. + * + * @see JsonTypeCoercer + * @see JsonInput + * @see JsonOutput + */ public class Json { + /** The value of {@code Content-Type} headers for HTTP requests and + * responses with JSON entities */ public static final String JSON_UTF_8 = "application/json; charset=utf-8"; + /** Specifier for {@code List} input/output type */ public static final Type LIST_OF_MAPS_TYPE = - new TypeToken>>() {}.getType(); + new TypeToken>>() {}.getType(); + /** Specifier for {@code Map} input/output type */ public static final Type MAP_TYPE = new TypeToken>() {}.getType(); + /** Specifier for {@code Object} input/output type */ public static final Type OBJECT_TYPE = new TypeToken() {}.getType(); private final JsonTypeCoercer fromJson = new JsonTypeCoercer(); + /** + * Serialize the specified object to JSON string representation.
+ * NOTE: This method limits traversal of nested objects to the default + * {@link JsonOutput#MAX_DEPTH maximum depth}. + * + * @param toConvert the object to be serialized + * @return JSON string representing the specified object + */ public String toJson(Object toConvert) { return toJson(toConvert, JsonOutput.MAX_DEPTH); } + /** + * Serialize the specified object to JSON string representation. + * + * @param toConvert the object to be serialized + * @param maxDepth maximum depth of nested object traversal + * @return JSON string representing the specified object + * @throws JsonException if an I/O exception is encountered + */ public String toJson(Object toConvert, int maxDepth) { try (Writer writer = new StringWriter(); - JsonOutput jsonOutput = newOutput(writer)) { + JsonOutput jsonOutput = newOutput(writer)) { jsonOutput.write(toConvert, maxDepth); return writer.toString(); } catch (IOException e) { @@ -51,10 +129,31 @@ public String toJson(Object toConvert, int maxDepth) { } } + /** + * Deserialize the specified JSON string into an object of the specified type.
+ * NOTE: This method uses the {@link PropertySetting#BY_NAME BY_NAME} strategy to assign values to properties + * in the deserialized object. + * + * @param source serialized source as JSON string + * @param typeOfT data type for deserialization (class or {@link TypeToken}) + * @return object of the specified type deserialized from [source] + * @param result type (as specified by [typeOfT]) + * @throws JsonException if an I/O exception is encountered + */ public T toType(String source, Type typeOfT) { return toType(source, typeOfT, PropertySetting.BY_NAME); } + /** + * Deserialize the specified JSON string into an object of the specified type. + * + * @param source serialized source as JSON string + * @param typeOfT data type for deserialization (class or {@link TypeToken}) + * @param setter strategy used to assign values during deserialization + * @return object of the specified type deserialized from [source] + * @param result type (as specified by [typeOfT]) + * @throws JsonException if an I/O exception is encountered + */ public T toType(String source, Type typeOfT, PropertySetting setter) { try (StringReader reader = new StringReader(source)) { return toType(reader, typeOfT, setter); @@ -63,10 +162,31 @@ public T toType(String source, Type typeOfT, PropertySetting setter) { } } + /** + * Deserialize the JSON string supplied by the specified {@code Reader} into an object of the specified type.
+ * NOTE: This method uses the {@link PropertySetting#BY_NAME BY_NAME} strategy to assign values to properties + * in the deserialized object. + * + * @param source {@link Reader} that supplies a serialized JSON string + * @param typeOfT data type for deserialization (class or {@link TypeToken}) + * @return object of the specified type deserialized from [source] + * @param result type (as specified by [typeOfT]) + * @throws JsonException if an I/O exception is encountered + */ public T toType(Reader source, Type typeOfT) { return toType(source, typeOfT, PropertySetting.BY_NAME); } + /** + * Deserialize the JSON string supplied by the specified {@code Reader} into an object of the specified type. + * + * @param source {@link Reader} that supplies a serialized JSON string + * @param typeOfT data type for deserialization (class or {@link TypeToken}) + * @param setter strategy used to assign values during deserialization + * @return object of the specified type deserialized from [source] + * @param result type (as specified by [typeOfT]) + * @throws JsonException if an I/O exception is encountered + */ public T toType(Reader source, Type typeOfT, PropertySetting setter) { if (setter == null) { throw new JsonException("Mechanism for setting properties must be set"); @@ -77,10 +197,26 @@ public T toType(Reader source, Type typeOfT, PropertySetting setter) { } } + /** + * Create a new {@code JsonInput} object to traverse the JSON string supplied the specified {@code Reader}.
+ * NOTE: The {@code JsonInput} object returned by this method uses the {@link PropertySetting#BY_NAME BY_NAME} + * strategy to assign values to properties objects it deserializes. + * + * @param from {@link Reader} that supplies a serialized JSON string + * @return {@link JsonInput} object to traverse the JSON string supplied by [from] + * @throws UncheckedIOException if an I/O exception occurs + */ public JsonInput newInput(Reader from) throws UncheckedIOException { return new JsonInput(from, fromJson, PropertySetting.BY_NAME); } + /** + * Create a new {@code JsonOutput} object to produce a serialized JSON string in the specified {@code Appendable}. + * + * @param to {@link Appendable} that consumes a serialized JSON string + * @return {@link JsonOutput} object to product a JSON string in [to] + * @throws UncheckedIOException if an I/O exception occurs + */ public JsonOutput newOutput(Appendable to) throws UncheckedIOException { return new JsonOutput(to); } diff --git a/java/src/org/openqa/selenium/json/JsonInput.java b/java/src/org/openqa/selenium/json/JsonInput.java index 808f2bb6607ee..e138ec73f6ed3 100644 --- a/java/src/org/openqa/selenium/json/JsonInput.java +++ b/java/src/org/openqa/selenium/json/JsonInput.java @@ -30,17 +30,30 @@ import java.util.function.Function; import org.openqa.selenium.internal.Require; +/** + * The JsonInput class defines the operations used to deserialize JSON strings into Java objects. + */ public class JsonInput implements Closeable { private final Reader source; + // FIXME: This flag is never set private volatile boolean readPerformed = false; private JsonTypeCoercer coercer; private PropertySetting setter; - private Input input; + private final Input input; // Used when reading maps and collections so that we handle de-nesting and // figuring out whether we're expecting a NAME properly. - private Deque stack = new ArrayDeque<>(); + private final Deque stack = new ArrayDeque<>(); + /** + * This package-private constructor initializes new instances of the {@code JsonInput} class.
+ * This class declares methods that enable structured traversal of the JSON string supplied by the + * {@code Reader} object specified by [source]. + * + * @param source {@link Reader} object that supplies the JSON string to be processed + * @param coercer {@link JsonTypeCoercer} that encapsulates the defined type-specific deserializers + * @param setter strategy used to assign values during deserialization + */ JsonInput(Reader source, JsonTypeCoercer coercer, PropertySetting setter) { this.source = Require.nonNull("Source", source); this.coercer = Require.nonNull("Coercer", coercer); @@ -60,10 +73,24 @@ public PropertySetting propertySetting(PropertySetting setter) { return previous; } + /** + * Add the specified type coercers to the set installed in the JSON coercion manager. + * + * @param coercers array of zero or more {@link TypeCoercer} objects + * @return this {@link JsonInput} object with added type coercers + * @throws JsonException if this {@code JsonInput} has already begun processing its input + */ public JsonInput addCoercers(TypeCoercer... coercers) { return addCoercers(Arrays.asList(coercers)); } + /** + * Add the specified type coercers to the set installed in the JSON coercion manager. + * + * @param coercers iterable collection of {@link TypeCoercer} objects + * @return this {@link JsonInput} object with added type coercers + * @throws JsonException if this {@code JsonInput} has already begun processing its input + */ public JsonInput addCoercers(Iterable> coercers) { synchronized (this) { if (readPerformed) { @@ -76,6 +103,11 @@ public JsonInput addCoercers(Iterable> coercers) { return this; } + /** + * {@inheritDoc} + * + * @throws UncheckedIOException if an I/O exception is encountered + */ @Override public void close() { try { @@ -85,6 +117,13 @@ public void close() { } } + /** + * Peek at the next input string character to determine the pending JSON element type. + * + * @return {@link JsonType} indicating the pending JSON element type + * @throws JsonException if unable to determine the type of the pending element + * @throws UncheckedIOException if an I/O exception is encountered + */ public JsonType peek() { skipWhitespace(input); @@ -134,11 +173,25 @@ public JsonType peek() { } } + /** + * Read the next element of the JSON input stream as a boolean value. + * + * @return {@code true} or {@code false} + * @throws JsonException if the next element isn't the expected boolean + * @throws UncheckedIOException if an I/O exception is encountered + */ public boolean nextBoolean() { expect(JsonType.BOOLEAN); return read(input.peek() == 't' ? "true" : "false", Boolean::valueOf); } + /** + * Read the next element of the JSON input stream as an object property name. + * + * @return JSON object property name + * @throws JsonException if the next element isn't a string followed by a colon + * @throws UncheckedIOException if an I/O exception is encountered + */ public String nextName() { expect(JsonType.NAME); @@ -147,16 +200,30 @@ public String nextName() { char read = input.read(); if (read != ':') { throw new JsonException( - "Unable to read name. Expected colon separator, but saw '" + read + "'"); + "Unable to read name. Expected colon separator, but saw '" + read + "'"); } return name; } + /** + * Read the next element of the JSON input stream as a {@code null} object. + * + * @return {@code null} object + * @throws JsonException if the next element isn't a {@code null} + * @throws UncheckedIOException if an I/O exception is encountered + */ public Object nextNull() { expect(JsonType.NULL); return read("null", str -> null); } + /** + * Read the next element of the JSON input stream as a number. + * + * @return {@link Number} object + * @throws JsonException if the next element isn't a number + * @throws UncheckedIOException if an I/O exception is encountered + */ public Number nextNumber() { expect(JsonType.NUMBER); StringBuilder builder = new StringBuilder(); @@ -165,19 +232,17 @@ public Number nextNumber() { do { char read = input.peek(); if (Character.isDigit(read) - || read == '+' - || read == '-' - || read == 'e' - || read == 'E' - || read == '.') { + || read == '+' + || read == '-' + || read == 'e' + || read == 'E' + || read == '.') { builder.append(input.read()); } else { break; } - if (read == '.') { - fractionalPart = true; - } + fractionalPart |= (read == '.'); } while (true); try { @@ -191,20 +256,41 @@ public Number nextNumber() { } } + /** + * Read the next element of the JSON input stream as a string. + * + * @return {@link String} object + * @throws JsonException if the next element isn't a string + * @throws UncheckedIOException if an I/O exception is encountered + */ public String nextString() { expect(JsonType.STRING); return readString(); } + /** + * Read the next element of the JSON input stream as an instant. + * + * @return {@link Instant} object + * @throws JsonException if the next element isn't a {@code Long} + * @throws UncheckedIOException if an I/O exception is encountered + */ public Instant nextInstant() { Long time = read(Long.class); return (null != time) ? Instant.ofEpochSecond(time) : null; } + /** + * Determine whether an element is pending for the current container from the JSON input stream. + * + * @return {@code true} if an element is pending; otherwise {@code false} + * @throws JsonException if no container is open + * @throws UncheckedIOException if an I/O exception is encountered + */ public boolean hasNext() { if (stack.isEmpty()) { throw new JsonException( - "Unable to determine if an item has next when not in a container type. " + input); + "Unable to determine if an item has next when not in a container type. " + input); } skipWhitespace(input); @@ -217,29 +303,49 @@ public boolean hasNext() { return type != JsonType.END_COLLECTION && type != JsonType.END_MAP; } + /** + * Process the opening square bracket of a JSON array. + * + * @throws UncheckedIOException if an I/O exception is encountered + */ public void beginArray() { expect(JsonType.START_COLLECTION); stack.addFirst(Container.COLLECTION); input.read(); } + /** + * Process the closing square bracket of a JSON array. + * + * @throws UncheckedIOException if an I/O exception is encountered + */ public void endArray() { expect(JsonType.END_COLLECTION); Container expectation = stack.removeFirst(); if (expectation != Container.COLLECTION) { // The only other thing we could be closing is a map throw new JsonException( - "Attempt to close a JSON List, but a JSON Object was expected. " + input); + "Attempt to close a JSON List, but a JSON Object was expected. " + input); } input.read(); } + /** + * Process the opening curly brace of a JSON object. + * + * @throws UncheckedIOException if an I/O exception is encountered + */ public void beginObject() { expect(JsonType.START_MAP); stack.addFirst(Container.MAP_NAME); input.read(); } + /** + * Process the closing curly brace of a JSON object. + * + * @throws UncheckedIOException if an I/O exception is encountered + */ public void endObject() { expect(JsonType.END_MAP); Container expectation = stack.removeFirst(); @@ -250,6 +356,14 @@ public void endObject() { input.read(); } + /** + * Discard the pending JSON property value. + * + * @throws JsonException if the pending element isn't a value type + * @throws UncheckedIOException if an I/O exception is encountered + */ + // FIXME: This method doesn't verify that the prior element was a property name. + // FIXME: This method doesn't enforce a depth limit when processing container types. public void skipValue() { switch (peek()) { case BOOLEAN: @@ -294,6 +408,16 @@ public void skipValue() { } } + /** + * Read the next element from the JSON input stream as the specified type. + * + * @param type data type for deserialization (class or {@link TypeToken}) + * @return object of the specified type deserialized from the JSON input stream
+ * NOTE: Returns {@code null} if the input string is exhausted. + * @param result type (as specified by [type]) + * @throws JsonException if coercion of the next element to the specified type fails + * @throws UncheckedIOException if an I/O exception is encountered + */ public T read(Type type) { skipWhitespace(input); @@ -305,14 +429,26 @@ public T read(Type type) { return coercer.coerce(this, type, setter); } + /** + * Determine if awaiting a JSON object property name. + * + * @return {@code true} is awaiting a property name; otherwise {@code false} + */ private boolean isReadingName() { return stack.peekFirst() == Container.MAP_NAME; } + /** + * Verify that the type of the pending JSON element matches the specified type. + * + * @param type expected JSON element type + * @throws JsonException if the pending element is not of the expected type + * @throws UncheckedIOException if an I/O exception is encountered + */ private void expect(JsonType type) { if (peek() != type) { throw new JsonException( - "Expected to read a " + type + " but instead have: " + peek() + ". " + input); + "Expected to read a " + type + " but instead have: " + peek() + ". " + input); } // Special map handling. Woo! @@ -337,6 +473,15 @@ private void expect(JsonType type) { } } + /** + * Read the next element from the JSON input stream, converting with the supplied mapper if it's the expected string. + * + * @param toCompare expected element string + * @param mapper function to convert the element string to its corresponding type + * @return value produced by the supplied mapper + * @param data type returned by the supplied mapper + * @throws UncheckedIOException if an I/O exception is encountered + */ private X read(String toCompare, Function mapper) { skipWhitespace(input); @@ -344,14 +489,21 @@ private X read(String toCompare, Function mapper) { char read = input.read(); if (read != toCompare.charAt(i)) { throw new JsonException( - String.format( - "Unable to read %s. Saw %s at position %d. %s", toCompare, read, i, input)); + String.format( + "Unable to read %s. Saw %s at position %d. %s", toCompare, read, i, input)); } } return mapper.apply(toCompare); } + /** + * Read the next element from the JSON input stream as a string, converting escaped characters. + * + * @return {@link String} object + * @throws JsonException if input stream ends without finding a closing quote + * @throws UncheckedIOException if an I/O exception is encountered + */ private String readString() { input.read(); // Skip leading quote @@ -373,6 +525,15 @@ private String readString() { } } + /** + * Convert the escape sequence at the current JSON input stream position, appending the result to the provided + * builder. + * + * @param builder {@link StringBuilder} + * @throws JsonException if an unsupported escape sequence is found + * @throws UncheckedIOException if an I/O exception is encountered + */ + // FIXME: This function doesn't appear to support UTF-8 or UTF-32. private void readEscape(StringBuilder builder) { char read = input.read(); @@ -424,15 +585,28 @@ private void readEscape(StringBuilder builder) { } } + /** + * Consume whitespace characters from the head of the specified input object. + * + * @param input {@link Input} object + * @throws UncheckedIOException if an I/O exception is encountered + */ private void skipWhitespace(Input input) { while (input.peek() != Input.EOF && Character.isWhitespace(input.peek())) { input.read(); } } + /** + * Used to track the current container processing state. + */ private enum Container { + + /** Processing a JSON array */ COLLECTION, + /** Processing a JSON object property name */ MAP_NAME, + /** Processing a JSON object property value */ MAP_VALUE, } } diff --git a/java/src/org/openqa/selenium/json/JsonOutput.java b/java/src/org/openqa/selenium/json/JsonOutput.java index acc6f21d9ee53..d580acfa7f9df 100644 --- a/java/src/org/openqa/selenium/json/JsonOutput.java +++ b/java/src/org/openqa/selenium/json/JsonOutput.java @@ -22,6 +22,7 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.lang.reflect.Method; import java.net.URI; import java.net.URL; @@ -46,9 +47,12 @@ import org.openqa.selenium.internal.Require; import org.openqa.selenium.logging.LogLevelMapping; +/** + * + */ public class JsonOutput implements Closeable { private static final Logger LOG = Logger.getLogger(JsonOutput.class.getName()); - static final int MAX_DEPTH = 100; + static final int MAX_DEPTH = 10; private static final Predicate> GSON_ELEMENT; @@ -103,7 +107,7 @@ public class JsonOutput implements Closeable { private final Map>, DepthAwareConsumer> converters; private final Appendable appendable; private final Consumer appender; - private Deque stack; + private final Deque stack; private String indent = ""; private String lineSeparator = "\n"; private String indentBy = " "; @@ -113,13 +117,13 @@ public class JsonOutput implements Closeable { this.appendable = Require.nonNull("Underlying appendable", appendable); this.appender = - str -> { - try { - appendable.append(str); - } catch (IOException e) { - throw new JsonException("Unable to write to underlying appendable", e); - } - }; + str -> { + try { + appendable.append(str); + } catch (IOException e) { + throw new JsonException("Unable to write to underlying appendable", e); + } + }; this.stack = new ArrayDeque<>(); this.stack.addFirst(new Empty()); @@ -129,147 +133,161 @@ public class JsonOutput implements Closeable { Map>, DepthAwareConsumer> builder = new LinkedHashMap<>(); builder.put(Objects::isNull, (obj, maxDepth, depthRemaining) -> append("null")); builder.put( - CharSequence.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> append(asString(obj))); + CharSequence.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> append(asString(obj))); builder.put( - Number.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(obj.toString())); + Number.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(obj.toString())); builder.put( - Boolean.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> append((Boolean) obj ? "true" : "false")); + Boolean.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> append((Boolean) obj ? "true" : "false")); builder.put( - Date.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> - append(String.valueOf(MILLISECONDS.toSeconds(((Date) obj).getTime())))); + Date.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> + append(String.valueOf(MILLISECONDS.toSeconds(((Date) obj).getTime())))); builder.put( - Instant.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> - append(asString(DateTimeFormatter.ISO_INSTANT.format((Instant) obj)))); + Instant.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> + append(asString(DateTimeFormatter.ISO_INSTANT.format((Instant) obj)))); builder.put( - Enum.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString(obj))); + Enum.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString(obj))); builder.put( - File.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> append(((File) obj).getAbsolutePath())); + File.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> append(((File) obj).getAbsolutePath())); builder.put( - URI.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> append(asString((obj).toString()))); + URI.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> append(asString((obj).toString()))); builder.put( - URL.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> append(asString(((URL) obj).toExternalForm()))); + URL.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> append(asString(((URL) obj).toExternalForm()))); builder.put( - UUID.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> append(asString(obj.toString()))); + UUID.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> append(asString(obj.toString()))); builder.put( - Level.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> append(asString(LogLevelMapping.getName((Level) obj)))); + Level.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> append(asString(LogLevelMapping.getName((Level) obj)))); builder.put( - GSON_ELEMENT, - (obj, maxDepth, depthRemaining) -> { - LOG.log( - Level.WARNING, - "Attempt to convert JsonElement from GSON. This functionality is deprecated. " - + "Diagnostic stacktrace follows", - new JsonException("Stack trace to determine cause of warning")); - append(obj.toString()); - }); + GSON_ELEMENT, + (obj, maxDepth, depthRemaining) -> { + LOG.log( + Level.WARNING, + "Attempt to convert JsonElement from GSON. This functionality is deprecated. " + + "Diagnostic stacktrace follows", + new JsonException("Stack trace to determine cause of warning")); + append(obj.toString()); + }); // Special handling of asMap and toJson builder.put( - cls -> getMethod(cls, "toJson") != null, - (obj, maxDepth, depthRemaining) -> - convertUsingMethod("toJson", obj, maxDepth, depthRemaining)); + cls -> getMethod(cls, "toJson") != null, + (obj, maxDepth, depthRemaining) -> + convertUsingMethod("toJson", obj, maxDepth, depthRemaining)); builder.put( - cls -> getMethod(cls, "asMap") != null, - (obj, maxDepth, depthRemaining) -> - convertUsingMethod("asMap", obj, maxDepth, depthRemaining)); + cls -> getMethod(cls, "asMap") != null, + (obj, maxDepth, depthRemaining) -> + convertUsingMethod("asMap", obj, maxDepth, depthRemaining)); builder.put( - cls -> getMethod(cls, "toMap") != null, - (obj, maxDepth, depthRemaining) -> - convertUsingMethod("toMap", obj, maxDepth, depthRemaining)); + cls -> getMethod(cls, "toMap") != null, + (obj, maxDepth, depthRemaining) -> + convertUsingMethod("toMap", obj, maxDepth, depthRemaining)); // And then the collection types builder.put( - Collection.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> { - if (depthRemaining < 1) { - throw new JsonException( - "Reached the maximum depth of " + maxDepth + " while writing JSON"); - } - beginArray(); - ((Collection) obj) - .stream() - .filter(o -> (!(o instanceof Optional) || ((Optional) o).isPresent())) - .forEach(o -> write0(o, maxDepth, depthRemaining - 1)); - endArray(); - }); + Collection.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> { + if (depthRemaining < 1) { + throw new JsonException( + "Reached the maximum depth of " + maxDepth + " while writing JSON"); + } + beginArray(); + ((Collection) obj) + .stream() + .filter(o -> (!(o instanceof Optional) || ((Optional) o).isPresent())) + .forEach(o -> write0(o, maxDepth, depthRemaining - 1)); + endArray(); + }); builder.put( - Map.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> { - if (depthRemaining < 1) { - throw new JsonException( - "Reached the maximum depth of " + maxDepth + " while writing JSON"); - } - beginObject(); - ((Map) obj) - .forEach( - (key, value) -> { - if (value instanceof Optional && !((Optional) value).isPresent()) { - return; - } - name(String.valueOf(key)).write0(value, maxDepth, depthRemaining - 1); - }); - endObject(); - }); + Map.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> { + if (depthRemaining < 1) { + throw new JsonException( + "Reached the maximum depth of " + maxDepth + " while writing JSON"); + } + beginObject(); + ((Map) obj) + .forEach( + (key, value) -> { + if (value instanceof Optional && !((Optional) value).isPresent()) { + return; + } + name(String.valueOf(key)).write0(value, maxDepth, depthRemaining - 1); + }); + endObject(); + }); builder.put( - Class::isArray, - (obj, maxDepth, depthRemaining) -> { - if (depthRemaining < 1) { - throw new JsonException( - "Reached the maximum depth of " + maxDepth + " while writing JSON"); - } - beginArray(); - Stream.of((Object[]) obj) - .filter(o -> (!(o instanceof Optional) || ((Optional) o).isPresent())) - .forEach(o -> write0(o, maxDepth, depthRemaining - 1)); - endArray(); - }); + Class::isArray, + (obj, maxDepth, depthRemaining) -> { + if (depthRemaining < 1) { + throw new JsonException( + "Reached the maximum depth of " + maxDepth + " while writing JSON"); + } + beginArray(); + Stream.of((Object[]) obj) + .filter(o -> (!(o instanceof Optional) || ((Optional) o).isPresent())) + .forEach(o -> write0(o, maxDepth, depthRemaining - 1)); + endArray(); + }); builder.put( - Optional.class::isAssignableFrom, - (obj, maxDepth, depthRemaining) -> { - Optional optional = (Optional) obj; - if (!optional.isPresent()) { - append("null"); - return; - } + Optional.class::isAssignableFrom, + (obj, maxDepth, depthRemaining) -> { + Optional optional = (Optional) obj; + if (!optional.isPresent()) { + append("null"); + return; + } - write0(optional.get(), maxDepth, depthRemaining); - }); + write0(optional.get(), maxDepth, depthRemaining); + }); // Finally, attempt to convert as an object builder.put( - cls -> true, - (obj, maxDepth, depthRemaining) -> { - if (depthRemaining < 1) { - throw new JsonException( - "Reached the maximum depth of " + maxDepth + " while writing JSON"); - } - mapObject(obj, maxDepth, depthRemaining - 1); - }); + cls -> true, + (obj, maxDepth, depthRemaining) -> { + if (depthRemaining < 1) { + throw new JsonException( + "Reached the maximum depth of " + maxDepth + " while writing JSON"); + } + mapObject(obj, maxDepth, depthRemaining - 1); + }); this.converters = Collections.unmodifiableMap(builder); } + /** + * + * @param enablePrettyPrinting + * @return + */ public JsonOutput setPrettyPrint(boolean enablePrettyPrinting) { this.lineSeparator = enablePrettyPrinting ? "\n" : ""; this.indentBy = enablePrettyPrinting ? " " : ""; return this; } + /** + * + * @param writeClassName + * @return + */ public JsonOutput writeClassName(boolean writeClassName) { this.writeClassName = writeClassName; return this; } + /** + * + * @return + */ public JsonOutput beginObject() { stack.getFirst().write("{" + lineSeparator); indent += indentBy; @@ -277,6 +295,11 @@ public JsonOutput beginObject() { return this; } + /** + * + * @param name + * @return + */ public JsonOutput name(String name) { if (!(stack.getFirst() instanceof JsonObject)) { throw new JsonException("Attempt to write name, but not writing a json object: " + name); @@ -285,6 +308,10 @@ public JsonOutput name(String name) { return this; } + /** + * + * @return + */ public JsonOutput endObject() { Node topOfStack = stack.getFirst(); if (!(topOfStack instanceof JsonObject)) { @@ -301,6 +328,10 @@ public JsonOutput endObject() { return this; } + /** + * + * @return + */ public JsonOutput beginArray() { append("[" + lineSeparator); indent += indentBy; @@ -308,6 +339,10 @@ public JsonOutput beginArray() { return this; } + /** + * + * @return + */ public JsonOutput endArray() { Node topOfStack = stack.getFirst(); if (!(topOfStack instanceof JsonCollection)) { @@ -324,25 +359,48 @@ public JsonOutput endArray() { return this; } + /** + * + * @param value + * @return + */ public JsonOutput write(Object value) { return write(value, MAX_DEPTH); } + /** + * + * @param value + * @param maxDepth + * @return + */ public JsonOutput write(Object value, int maxDepth) { return write0(value, maxDepth, maxDepth); } + /** + * + * @param input + * @param maxDepth + * @param depthRemaining + * @return + */ private JsonOutput write0(Object input, int maxDepth, int depthRemaining) { converters.entrySet().stream() - .filter(entry -> entry.getKey().test(input == null ? null : input.getClass())) - .findFirst() - .map(Map.Entry::getValue) - .orElseThrow(() -> new JsonException("Unable to write " + input)) - .consume(input, maxDepth, depthRemaining); + .filter(entry -> entry.getKey().test(input == null ? null : input.getClass())) + .findFirst() + .map(Map.Entry::getValue) + .orElseThrow(() -> new JsonException("Unable to write " + input)) + .consume(input, maxDepth, depthRemaining); return this; } + /** + * {@inheritDoc} + * + * @throws JsonException if JSON stream isn't empty or an I/O exception is encountered + */ @Override public void close() { if (appendable instanceof Closeable) { @@ -358,31 +416,47 @@ public void close() { } } + /** + * + * @param text + * @return + */ private JsonOutput append(String text) { stack.getFirst().write(text); return this; } + /** + * + * @param obj + * @return + */ private String asString(Object obj) { StringBuilder toReturn = new StringBuilder("\""); String.valueOf(obj) - .chars() - .forEach( - i -> { - String escaped = ESCAPES.get(i); - if (escaped != null) { - toReturn.append(escaped); - } else { - toReturn.append((char) i); - } - }); + .chars() + .forEach( + i -> { + String escaped = ESCAPES.get(i); + if (escaped != null) { + toReturn.append(escaped); + } else { + toReturn.append((char) i); + } + }); toReturn.append('"'); return toReturn.toString(); } + /** + * + * @param clazz + * @param methodName + * @return + */ private Method getMethod(Class clazz, String methodName) { if (Object.class.equals(clazz)) { return null; @@ -396,17 +470,25 @@ private Method getMethod(Class clazz, String methodName) { return getMethod(clazz.getSuperclass(), methodName); } catch (SecurityException e) { throw new JsonException( - "Unable to find the method because of a security constraint: " + methodName, e); + "Unable to find the method because of a security constraint: " + methodName, e); } } + /** + * + * @param methodName + * @param toConvert + * @param maxDepth + * @param depthRemaining + * @return + */ private JsonOutput convertUsingMethod( - String methodName, Object toConvert, int maxDepth, int depthRemaining) { + String methodName, Object toConvert, int maxDepth, int depthRemaining) { try { Method method = getMethod(toConvert.getClass(), methodName); if (method == null) { throw new JsonException( - String.format("Unable to read object %s using method %s", toConvert, methodName)); + String.format("Unable to read object %s using method %s", toConvert, methodName)); } Object value = method.invoke(toConvert); @@ -416,6 +498,12 @@ private JsonOutput convertUsingMethod( } } + /** + * + * @param toConvert + * @param maxDepth + * @param depthRemaining + */ private void mapObject(Object toConvert, int maxDepth, int depthRemaining) { if (toConvert instanceof Class) { write(((Class) toConvert).getName()); @@ -425,7 +513,7 @@ private void mapObject(Object toConvert, int maxDepth, int depthRemaining) { // Raw object via reflection? Nope, not needed beginObject(); for (SimplePropertyDescriptor pd : - SimplePropertyDescriptor.getPropertyDescriptors(toConvert.getClass())) { + SimplePropertyDescriptor.getPropertyDescriptors(toConvert.getClass())) { // Only include methods not on java.lang.Object to stop things being super-noisy Function readMethod = pd.getReadMethod(); @@ -446,9 +534,16 @@ private void mapObject(Object toConvert, int maxDepth, int depthRemaining) { endObject(); } + /** + * + */ private class Node { protected boolean isEmpty = true; + /** + * + * @param text + */ public void write(String text) { if (isEmpty) { isEmpty = false; @@ -461,6 +556,9 @@ public void write(String text) { } } + /** + * + */ private class Empty extends Node { @Override @@ -473,11 +571,21 @@ public void write(String text) { } } + /** + * + */ private class JsonCollection extends Node {} + /** + * + */ private class JsonObject extends Node { private boolean isNameNext = true; + /** + * + * @param name + */ public void name(String name) { if (!isNameNext) { throw new JsonException("Unexpected attempt to set name of json object: " + name); @@ -498,8 +606,17 @@ public void write(String text) { } } + /** + * + */ @FunctionalInterface private interface DepthAwareConsumer { + + /** + * @param object + * @param maxDepth + * @param depthRemaining + */ void consume(Object object, int maxDepth, int depthRemaining); } } diff --git a/java/src/org/openqa/selenium/json/JsonType.java b/java/src/org/openqa/selenium/json/JsonType.java index db391ebe49fd1..c38bac047a74b 100644 --- a/java/src/org/openqa/selenium/json/JsonType.java +++ b/java/src/org/openqa/selenium/json/JsonType.java @@ -17,15 +17,28 @@ package org.openqa.selenium.json; +/** + * Used to specify the pending JSON element type. + */ public enum JsonType { + /** Boolean value */ BOOLEAN, + /** property name */ NAME, + /** {@code null} value */ NULL, + /** numeric value */ NUMBER, + /** start of object */ START_MAP, + /** end of object */ END_MAP, + /** start of array */ START_COLLECTION, + /** end of array */ END_COLLECTION, + /** string value */ STRING, + /** end of input */ END } diff --git a/java/src/org/openqa/selenium/json/JsonTypeCoercer.java b/java/src/org/openqa/selenium/json/JsonTypeCoercer.java index 246e91dcecd5f..910e40c71cc6a 100644 --- a/java/src/org/openqa/selenium/json/JsonTypeCoercer.java +++ b/java/src/org/openqa/selenium/json/JsonTypeCoercer.java @@ -42,27 +42,42 @@ import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.internal.Require; +/** + * + */ class JsonTypeCoercer { private final Set> additionalCoercers; private final Set> coercers; private final Map> knownCoercers = - new ConcurrentHashMap<>(); + new ConcurrentHashMap<>(); + /** + * + */ JsonTypeCoercer() { this(Stream.of()); } + /** + * + * @param coercer + * @param coercers + */ JsonTypeCoercer(JsonTypeCoercer coercer, Iterable> coercers) { this( - Stream.concat( - StreamSupport.stream(coercers.spliterator(), false), - coercer.additionalCoercers.stream())); + Stream.concat( + StreamSupport.stream(coercers.spliterator(), false), + coercer.additionalCoercers.stream())); } + /** + * + * @param coercers + */ private JsonTypeCoercer(Stream> coercers) { this.additionalCoercers = - coercers.collect(collectingAndThen(toSet(), Collections::unmodifiableSet)); + coercers.collect(collectingAndThen(toSet(), Collections::unmodifiableSet)); // Note: we call out when ordering matters. Set> builder = new LinkedHashSet<>(additionalCoercers); @@ -77,15 +92,15 @@ private JsonTypeCoercer(Stream> coercers) { builder.add(new NumberCoercer<>(Integer.class, Number::intValue)); builder.add(new NumberCoercer<>(Long.class, Number::longValue)); builder.add( - new NumberCoercer<>( - Number.class, - num -> { - double doubleValue = num.doubleValue(); - if (doubleValue % 1 != 0 || doubleValue > Long.MAX_VALUE) { - return doubleValue; - } - return num.longValue(); - })); + new NumberCoercer<>( + Number.class, + num -> { + double doubleValue = num.doubleValue(); + if (doubleValue % 1 != 0 || doubleValue > Long.MAX_VALUE) { + return doubleValue; + } + return num.longValue(); + })); builder.add(new NumberCoercer<>(Short.class, Number::shortValue)); builder.add(new StringCoercer()); builder.add(new EnumCoercer<>()); @@ -96,14 +111,14 @@ private JsonTypeCoercer(Stream> coercers) { // From Selenium builder.add( - new MapCoercer<>( - Capabilities.class, - this, - Collector.of( - MutableCapabilities::new, - (caps, entry) -> caps.setCapability((String) entry.getKey(), entry.getValue()), - MutableCapabilities::merge, - UNORDERED))); + new MapCoercer<>( + Capabilities.class, + this, + Collector.of( + MutableCapabilities::new, + (caps, entry) -> caps.setCapability((String) entry.getKey(), entry.getValue()), + MutableCapabilities::merge, + UNORDERED))); // Container types //noinspection unchecked @@ -114,18 +129,18 @@ private JsonTypeCoercer(Stream> coercers) { builder.add(new StaticInitializerCoercer()); builder.add( - new MapCoercer<>( - Map.class, - this, - Collector.of( - LinkedHashMap::new, - (map, entry) -> map.put(entry.getKey(), entry.getValue()), - (l, r) -> { - l.putAll(r); - return l; - }, - UNORDERED, - CONCURRENT))); + new MapCoercer<>( + Map.class, + this, + Collector.of( + LinkedHashMap::new, + (map, entry) -> map.put(entry.getKey(), entry.getValue()), + (l, r) -> { + l.putAll(r); + return l; + }, + UNORDERED, + CONCURRENT))); // If the requested type is exactly "Object", do some guess work builder.add(new ObjectCoercer(this)); @@ -136,9 +151,18 @@ private JsonTypeCoercer(Stream> coercers) { this.coercers = Collections.unmodifiableSet(builder); } + /** + * Deserialize the next JSON element as an object of the specified type. + * + * @param json serialized source as JSON string + * @param typeOfT data type for deserialization (class or {@link TypeToken}) + * @param setter strategy used to assign values during deserialization + * @return object of the specified type deserialized from [source] + * @param result type (as specified by [typeOfT]) + */ T coerce(JsonInput json, Type typeOfT, PropertySetting setter) { BiFunction coercer = - knownCoercers.computeIfAbsent(typeOfT, this::buildCoercer); + knownCoercers.computeIfAbsent(typeOfT, this::buildCoercer); // We need to keep null checkers happy, apparently. @SuppressWarnings("unchecked") @@ -147,21 +171,26 @@ T coerce(JsonInput json, Type typeOfT, PropertySetting setter) { return result; } + /** + * + * @param type + * @return + */ private BiFunction buildCoercer(Type type) { return coercers.stream() - .filter(coercer -> coercer.test(narrow(type))) - .findFirst() - .map(coercer -> coercer.apply(type)) - .map( - func -> - (BiFunction) - (jsonInput, setter) -> { - if (jsonInput.peek() == JsonType.NULL) { - return jsonInput.nextNull(); - } - - return func.apply(jsonInput, setter); - }) - .orElseThrow(() -> new JsonException("Unable to find type coercer for " + type)); + .filter(coercer -> coercer.test(narrow(type))) + .findFirst() + .map(coercer -> coercer.apply(type)) + .map( + func -> + (BiFunction) + (jsonInput, setter) -> { + if (jsonInput.peek() == JsonType.NULL) { + return jsonInput.nextNull(); + } + + return func.apply(jsonInput, setter); + }) + .orElseThrow(() -> new JsonException("Unable to find type coercer for " + type)); } } diff --git a/java/src/org/openqa/selenium/json/PropertySetting.java b/java/src/org/openqa/selenium/json/PropertySetting.java index 4a6d9188479b5..60724594220b1 100644 --- a/java/src/org/openqa/selenium/json/PropertySetting.java +++ b/java/src/org/openqa/selenium/json/PropertySetting.java @@ -17,7 +17,18 @@ package org.openqa.selenium.json; +import java.io.Reader; +import java.lang.reflect.Type; + +/** + * Used to specify the strategy used to assign values during deserialization. + * + * @see org.openqa.selenium.json.Json#toType(Reader, Type, PropertySetting) + * @see org.openqa.selenium.json.Json#toType(String, Type, PropertySetting) + */ public enum PropertySetting { + /** Values are stored via the corresponding Bean setter methods. */ BY_NAME, + /** Values are stored in fields with the indicated names. */ BY_FIELD }