Java tutorial
// 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.UncheckedIOException; import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.Instant; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; import java.util.Objects; import java.util.function.Function; public class JsonInput implements Closeable { private final Readable source; private volatile boolean readPerformed = false; private JsonTypeCoercer coercer; private PropertySetting setter; private 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<Container> stack = new ArrayDeque<>(); JsonInput(Readable source, JsonTypeCoercer coercer, PropertySetting setter) { this.source = Objects.requireNonNull(source); this.coercer = Objects.requireNonNull(coercer); this.input = new Input(source); this.setter = Objects.requireNonNull(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 = Objects.requireNonNull(setter); return previous; } public JsonInput addCoercers(TypeCoercer<?>... coercers) { return addCoercers(Arrays.asList(coercers)); } public JsonInput addCoercers(Iterable<TypeCoercer<?>> 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; } @Override public void close() { if (source instanceof Closeable) { try { ((Closeable) source).close(); } catch (IOException e) { throw new UncheckedIOException(e); } } } 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); } } public boolean nextBoolean() { expect(JsonType.BOOLEAN); return read(input.peek() == 't' ? "true" : "false", Boolean::valueOf); } 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; } public Object nextNull() { expect(JsonType.NULL); return read("null", str -> null); } 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; } if (read == '.') { fractionalPart = true; } } 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); } } public String nextString() { expect(JsonType.STRING); return readString(); } public Instant nextInstant() { Long time = read(Long.class); return (null != time) ? Instant.ofEpochSecond(time) : null; } 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; } public void beginArray() { expect(JsonType.START_COLLECTION); stack.addFirst(Container.COLLECTION); input.read(); } 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(); } public void beginObject() { expect(JsonType.START_MAP); stack.addFirst(Container.MAP_NAME); input.read(); } 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(); } 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); } } public <T> T read(Type type) { return coercer.coerce(this, type, setter); } private boolean isReadingName() { return stack.peekFirst() == Container.MAP_NAME; } 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); } } private <X> X read(String toCompare, Function<String, X> 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); } private String readString() { input.read(); // Skip leading quote StringBuilder builder = new StringBuilder(); while (input.peek() != '"' && input.peek() != Input.EOF) { char read = input.read(); if (read == '\\') { readEscape(builder); } else { builder.append(read); } } char last = input.read();// Skip trailing quote if (last != '"') { throw new JsonException("Unterminated string: " + builder + ". " + input); } return builder.toString(); } 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); } } private void skipWhitespace(Input input) { while (input.peek() != Input.EOF && Character.isWhitespace(input.peek())) { input.read(); } } private enum Container { COLLECTION, MAP_NAME, MAP_VALUE, } }