org.openqa.selenium.json.JsonInput Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of selenium-json Show documentation
Show all versions of selenium-json 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.json;
import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
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 final boolean readPerformed = false;
private JsonTypeCoercer coercer;
private PropertySetting setter;
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 final Deque stack = new ArrayDeque<>();
JsonInput(Reader source, JsonTypeCoercer coercer, PropertySetting setter) {
this.source = Require.nonNull("Source", source);
this.coercer = Require.nonNull("Coercer", coercer);
this.input = new Input(source);
this.setter = Require.nonNull("Setter", setter);
}
/**
* Change how property setting is done. It's polite to set the value back once done processing.
*
* @param setter The new {@link PropertySetting} to use.
* @return The previous {@link PropertySetting} that has just been replaced.
*/
public PropertySetting propertySetting(PropertySetting setter) {
PropertySetting previous = this.setter;
this.setter = Require.nonNull("Setter", 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) {
throw new JsonException("JsonInput has already been used and may not be modified");
}
this.coercer = new JsonTypeCoercer(coercer, coercers);
}
return this;
}
/**
* {@inheritDoc}
*
* @throws UncheckedIOException if an I/O exception is encountered
*/
@Override
public void close() {
try {
source.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* 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);
switch (input.peek()) {
case 'f':
case 't':
return JsonType.BOOLEAN;
case 'n':
return JsonType.NULL;
case '-':
case '+':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return JsonType.NUMBER;
case '"':
return isReadingName() ? JsonType.NAME : JsonType.STRING;
case '{':
return JsonType.START_MAP;
case '}':
return JsonType.END_MAP;
case '[':
return JsonType.START_COLLECTION;
case ']':
return JsonType.END_COLLECTION;
case Input.EOF:
return JsonType.END;
default:
char c = input.read();
throw new JsonException("Unable to determine type from: " + c + ". " + input);
}
}
/**
* 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);
String name = readString();
skipWhitespace(input);
char read = input.read();
if (read != ':') {
throw new JsonException(
"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();
// We know it's safe to use a do/while loop since the first character was a number
boolean fractionalPart = false;
do {
char read = input.peek();
if (Character.isDigit(read)
|| read == '+'
|| read == '-'
|| read == 'e'
|| read == 'E'
|| read == '.') {
builder.append(input.read());
} else {
break;
}
fractionalPart |= (read == '.');
} while (true);
try {
Number number = new BigDecimal(builder.toString());
if (fractionalPart) {
return number.doubleValue();
}
return number.longValue();
} catch (NumberFormatException e) {
throw new JsonException("Unable to parse to a number: " + builder.toString() + ". " + input);
}
}
/**
* 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);
}
skipWhitespace(input);
if (input.peek() == ',') {
input.read();
return true;
}
JsonType type = peek();
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);
}
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();
if (expectation != Container.MAP_NAME) {
// The only other thing we could be closing is a map
throw new JsonException("Attempt to close a JSON Map, but not ready to. " + input);
}
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:
nextBoolean();
break;
case NAME:
nextName();
break;
case NULL:
nextNull();
break;
case NUMBER:
nextNumber();
break;
case START_COLLECTION:
beginArray();
while (hasNext()) {
skipValue();
}
endArray();
break;
case START_MAP:
beginObject();
while (hasNext()) {
nextName();
skipValue();
}
endObject();
break;
case STRING:
nextString();
break;
default:
throw new JsonException("Cannot skip " + peek() + ". " + input);
}
}
/**
* 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);
// Guard against reading an empty stream
if (input.peek() == Input.EOF) {
return null;
}
return coercer.coerce(this, type, setter);
}
/**
* Read an array of elements from the JSON input stream with elements as the specified type.
*
* @param type data type for deserialization (class or {@link TypeToken})
* @return list of objects of the specified type deserialized from the JSON input stream
* NOTE: Returns {@code null} if the input string is exhausted.
* @param result type of the item in the list (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 List readArray(Type type) {
List toReturn = new ArrayList<>();
beginArray();
while (hasNext()) {
toReturn.add(coercer.coerce(this, type, setter));
}
endArray();
return toReturn;
}
/**
* 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);
}
// Special map handling. Woo!
Container top = stack.peekFirst();
if (type == JsonType.NAME) {
if (top == Container.MAP_NAME) {
stack.removeFirst();
stack.addFirst(Container.MAP_VALUE);
return;
} else if (top != null) {
throw new JsonException("Unexpected attempt to read name. " + input);
}
return; // End of Name handling
}
// Handle the case where we're reading a value
if (top == Container.MAP_VALUE) {
stack.removeFirst();
stack.addFirst(Container.MAP_NAME);
}
}
/**
* 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);
for (int i = 0; i < toCompare.length(); i++) {
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));
}
}
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
StringBuilder builder = new StringBuilder();
char c;
while (true) {
c = input.read();
switch (c) {
case Input.EOF:
throw new JsonException("Unterminated string: " + builder + ". " + input);
case '"': // terminate string
return builder.toString();
case '\\': // quoted char
readEscape(builder);
break;
default:
builder.append(c);
}
}
}
/**
* 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();
// List from: https://tools.ietf.org/html/rfc7159.html#section-7
switch (read) {
case 'b':
builder.append("\b");
break;
case 'f':
builder.append("\f");
break;
case 'n':
builder.append("\n");
break;
case 'r':
builder.append("\r");
break;
case 't':
builder.append("\t");
break;
case 'u': // Unicode digit. The next four characters count.
int result = 0;
int multiplier = 4096; // (16 * 16 * 16) as we start from the thousands and work to units.
for (int i = 0; i < 4; i++) {
char c = input.read();
int digit = Character.digit(c, 16);
if (digit == -1) {
throw new JsonException(c + " is not a hexadecimal digit. " + input);
}
result += digit * multiplier;
multiplier /= 16;
}
builder.append((char) result);
break;
case '/':
case '\\':
case '"':
builder.append(read);
break;
default:
throw new JsonException("Unexpected escape code: " + read + ". " + input);
}
}
/**
* 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,
}
}