com.github.nmorel.gwtjackson.client.stream.impl.NonBufferedJsonReader.java Source code

Java tutorial

Introduction

Here is the source code for com.github.nmorel.gwtjackson.client.stream.impl.NonBufferedJsonReader.java

Source

//@formatter:off
/*
 * Copyright (C) 2010 Google Inc.
 *
 * Licensed 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 com.github.nmorel.gwtjackson.client.stream.impl;

import java.math.BigInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.github.nmorel.gwtjackson.client.exception.JsonDeserializationException;
import com.github.nmorel.gwtjackson.client.stream.JsonToken;
import com.github.nmorel.gwtjackson.client.stream.JsonWriter;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayInteger;
import com.google.gwt.core.client.JsonUtils;

/**
 * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
 * encoded value as a stream of tokens. This stream includes both literal
 * values (strings, numbers, booleans, and nulls) as well as the begin and
 * end delimiters of objects and arrays. The tokens are traversed in
 * depth-first order, the same order that they appear in the JSON document.
 * Within JSON objects, name/value pairs are represented by a single token.
 *
 * <h3>Parsing JSON</h3>
 * To create a recursive descent parser for your own JSON streams, first create
 * an entry point method that creates a {@code JsonReader}.
 *
 * <p>Next, create handler methods for each structure in your JSON text. You'll
 * need a method for each object type and for each array type.
 * <ul>
 *   <li>Within <strong>array handling</strong> methods, first call {@link
 *       #beginArray} to consume the array's opening bracket. Then create a
 *       while loop that accumulates values, terminating when {@link #hasNext}
 *       is false. Finally, read the array's closing bracket by calling {@link
 *       #endArray}.
 *   <li>Within <strong>object handling</strong> methods, first call {@link
 *       #beginObject} to consume the object's opening brace. Then create a
 *       while loop that assigns values to local variables based on their name.
 *       This loop should terminate when {@link #hasNext} is false. Finally,
 *       read the object's closing brace by calling {@link #endObject}.
 * </ul>
 * <p>When a nested object or array is encountered, delegate to the
 * corresponding handler method.
 *
 * <p>When an unknown name is encountered, strict parsers should fail with an
 * exception. Lenient parsers should call {@link #skipValue()} to recursively
 * skip the value's nested tokens, which may otherwise conflict.
 *
 * <p>If a value may be null, you should first check using {@link #peek()}.
 * Null literals can be consumed using either {@link #nextNull()} or {@link
 * #skipValue()}.
 *
 * <h3>Example</h3>
 * Suppose we'd like to parse a stream of messages such as the following: <pre> {@code
 * [
 *   {
 *     "id": 912345678901,
 *     "text": "How do I read a JSON stream in Java?",
 *     "geo": null,
 *     "user": {
 *       "name": "json_newb",
 *       "followers_count": 41
 *      }
 *   },
 *   {
 *     "id": 912345678902,
 *     "text": "@json_newb just use JsonReader!",
 *     "geo": [50.454722, -104.606667],
 *     "user": {
 *       "name": "jesse",
 *       "followers_count": 2
 *     }
 *   }
 * ]}</pre>
 * This code implements the parser for the above structure: <pre>   {@code
 *
 *   public List<Message> readJsonStream(InputStream in) {
 *     JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
 *     try {
 *       return readMessagesArray(reader);
 *     } finally {
 *       reader.close();
 *     }
 *   }
 *
 *   public List<Message> readMessagesArray(JsonReader reader) {
 *     List<Message> messages = new ArrayList<Message>();
 *
 *     reader.beginArray();
 *     while (reader.hasNext()) {
 *       messages.add(readMessage(reader));
 *     }
 *     reader.endArray();
 *     return messages;
 *   }
 *
 *   public Message readMessage(JsonReader reader) {
 *     long id = -1;
 *     String text = null;
 *     User user = null;
 *     List<Double> geo = null;
 *
 *     reader.beginObject();
 *     while (reader.hasNext()) {
 *       String name = reader.nextName();
 *       if (name.equals("id")) {
 *         id = reader.nextLong();
 *       } else if (name.equals("text")) {
 *         text = reader.nextString();
 *       } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
 *         geo = readDoublesArray(reader);
 *       } else if (name.equals("user")) {
 *         user = readUser(reader);
 *       } else {
 *         reader.skipValue();
 *       }
 *     }
 *     reader.endObject();
 *     return new Message(id, text, user, geo);
 *   }
 *
 *   public List<Double> readDoublesArray(JsonReader reader) {
 *     List<Double> doubles = new ArrayList<Double>();
 *
 *     reader.beginArray();
 *     while (reader.hasNext()) {
 *       doubles.add(reader.nextDouble());
 *     }
 *     reader.endArray();
 *     return doubles;
 *   }
 *
 *   public User readUser(JsonReader reader) {
 *     String username = null;
 *     int followersCount = -1;
 *
 *     reader.beginObject();
 *     while (reader.hasNext()) {
 *       String name = reader.nextName();
 *       if (name.equals("name")) {
 *         username = reader.nextString();
 *       } else if (name.equals("followers_count")) {
 *         followersCount = reader.nextInt();
 *       } else {
 *         reader.skipValue();
 *       }
 *     }
 *     reader.endObject();
 *     return new User(username, followersCount);
 *   }}</pre>
 *
 * <h3>Number Handling</h3>
 * This reader permits numeric values to be read as strings and string values to
 * be read as numbers. For example, both elements of the JSON array {@code
 * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}.
 * This behavior is intended to prevent lossy numeric conversions: double is
 * JavaScript's only numeric type and very large values like {@code
 * 9007199254740993} cannot be represented exactly on that platform. To minimize
 * precision loss, extremely large values should be written and read as strings
 * in JSON.
 *
 * <h3>Non-Execute Prefix</h3>
 * Web servers that serve private data using JSON may be vulnerable to <a
 * href="http://en.wikipedia.org/wiki/JSON#Cross-site_request_forgery">Cross-site
 * request forgery</a> attacks. In such an attack, a malicious site gains access
 * to a private JSON file by executing it with an HTML {@code <script>} tag.
 *
 * <p>Prefixing JSON files with <code>")]}'\n"</code> makes them non-executable
 * by {@code <script>} tags, disarming the attack. Since the prefix is malformed
 * JSON, strict parsing fails when it is encountered. This class permits the
 * non-execute prefix when {@link #setLenient(boolean) lenient parsing} is
 * enabled.
 *
 * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances
 * of this class are not thread safe.
 *
 * @author Jesse Wilson
 * @since 1.6
 */
public class NonBufferedJsonReader implements com.github.nmorel.gwtjackson.client.stream.JsonReader {
    private static final Logger logger = Logger.getLogger("JsonReader");

    /** The only non-execute prefix this parser permits */
    private static final char[] NON_EXECUTE_PREFIX = ")]}'\n".toCharArray();
    private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;

    private final static long MIN_INT_L = (long) Integer.MIN_VALUE;
    private final static long MAX_INT_L = (long) Integer.MAX_VALUE;

    private final static BigInteger MIN_LONG_BIGINTEGER = new BigInteger("" + Long.MIN_VALUE);
    private final static BigInteger MAX_LONG_BIGINTEGER = new BigInteger("" + Long.MAX_VALUE);

    private static final int PEEKED_NONE = 0;
    private static final int PEEKED_BEGIN_OBJECT = 1;
    private static final int PEEKED_END_OBJECT = 2;
    private static final int PEEKED_BEGIN_ARRAY = 3;
    private static final int PEEKED_END_ARRAY = 4;
    private static final int PEEKED_TRUE = 5;
    private static final int PEEKED_FALSE = 6;
    private static final int PEEKED_NULL = 7;
    private static final int PEEKED_SINGLE_QUOTED = 8;
    private static final int PEEKED_DOUBLE_QUOTED = 9;
    private static final int PEEKED_UNQUOTED = 10;
    /** When this is returned, the string value is stored in peekedString. */
    private static final int PEEKED_BUFFERED = 11;
    private static final int PEEKED_SINGLE_QUOTED_NAME = 12;
    private static final int PEEKED_DOUBLE_QUOTED_NAME = 13;
    private static final int PEEKED_UNQUOTED_NAME = 14;
    /** When this is returned, the integer value is stored in peekedLong. */
    private static final int PEEKED_LONG = 15;
    private static final int PEEKED_NUMBER = 16;
    private static final int PEEKED_EOF = 17;

    /* State machine when parsing numbers */
    private static final int NUMBER_CHAR_NONE = 0;
    private static final int NUMBER_CHAR_SIGN = 1;
    private static final int NUMBER_CHAR_DIGIT = 2;
    private static final int NUMBER_CHAR_DECIMAL = 3;
    private static final int NUMBER_CHAR_FRACTION_DIGIT = 4;
    private static final int NUMBER_CHAR_EXP_E = 5;
    private static final int NUMBER_CHAR_EXP_SIGN = 6;
    private static final int NUMBER_CHAR_EXP_DIGIT = 7;

    /** The input JSON. */
    private final String in;

    /** True to accept non-spec compliant JSON */
    private boolean lenient = false;

    private int pos = 0;
    private final int limit;

    private int lineNumber = 0;
    private int lineStart = 0;

    private int peeked = PEEKED_NONE;

    /**
     * A peeked value that was composed entirely of digits with an optional
     * leading dash. Positive values may not have a leading 0.
     */
    private long peekedLong;

    /**
     * The number of characters in a peeked number literal. Increment 'pos' by
     * this after reading a number.
     */
    private int peekedNumberLength;

    /**
     * A peeked string that should be parsed on the next double, long or string.
     * This is populated before a numeric value is parsed and used if that parsing
     * fails.
     */
    private String peekedString;

    /*
     * The nesting stack. Using a manual array rather than an ArrayList saves 20%.
     */
    private JsArrayInteger stack = JsArrayInteger.createArray().cast();
    private int stackSize = 0;
    {
        stack.set(stackSize++, JsonScope.EMPTY_DOCUMENT);
    }

    /**
     * Creates a new instance that reads a JSON-encoded stream from {@code in}.
     */
    public NonBufferedJsonReader(String in) {
        if (in == null) {
            throw new NullPointerException("in == null");
        }
        this.in = in;
        this.limit = in.length();
        if (limit > 0 && in.charAt(0) == '\ufeff') {
            pos++;
            lineStart++;
        }
    }

    /**
     * Configure this parser to be  be liberal in what it accepts. By default,
     * this parser is strict and only accepts JSON as specified by <a
     * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the
     * parser to lenient causes it to ignore the following syntax errors:
     *
     * <ul>
     *   <li>Streams that start with the <a href="#nonexecuteprefix">non-execute
     *       prefix</a>, <code>")]}'\n"</code>.
     *   <li>Streams that include multiple top-level values. With strict parsing,
     *       each stream must contain exactly one top-level value.
     *   <li>Top-level values of any type. With strict parsing, the top-level
     *       value must be an object or an array.
     *   <li>Numbers may be {@link Double#isNaN() NaNs} or {@link
     *       Double#isInfinite() infinities}.
     *   <li>End of line comments starting with {@code //} or {@code #} and
     *       ending with a newline character.
     *   <li>C-style comments starting with {@code /*} and ending with
     *       {@code *}{@code /}. Such comments may not be nested.
     *   <li>Names that are unquoted or {@code 'single quoted'}.
     *   <li>Strings that are unquoted or {@code 'single quoted'}.
     *   <li>Array elements separated by {@code ;} instead of {@code ,}.
     *   <li>Unnecessary array separators. These are interpreted as if null
     *       was the omitted value.
     *   <li>Names and values separated by {@code =} or {@code =>} instead of
     *       {@code :}.
     *   <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
     * </ul>
     */
    public final void setLenient(boolean lenient) {
        this.lenient = lenient;
    }

    /**
     * Returns true if this parser is liberal in what it accepts.
     */
    public final boolean isLenient() {
        return lenient;
    }

    @Override
    public void beginArray() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_BEGIN_ARRAY) {
            push(JsonScope.EMPTY_ARRAY);
            peeked = PEEKED_NONE;
        } else {
            throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }
    }

    @Override
    public void endArray() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_END_ARRAY) {
            stackSize--;
            peeked = PEEKED_NONE;
        } else {
            throw new IllegalStateException("Expected END_ARRAY but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }
    }

    @Override
    public void beginObject() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_BEGIN_OBJECT) {
            push(JsonScope.EMPTY_OBJECT);
            peeked = PEEKED_NONE;
        } else {
            throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek() + " at line "
                    + getLineNumber() + " column " + getColumnNumber());
        }
    }

    @Override
    public void endObject() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_END_OBJECT) {
            stackSize--;
            peeked = PEEKED_NONE;
        } else {
            throw new IllegalStateException("Expected END_OBJECT but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }
    }

    @Override
    public boolean hasNext() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY;
    }

    @Override
    public JsonToken peek() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        switch (p) {
        case PEEKED_BEGIN_OBJECT:
            return JsonToken.BEGIN_OBJECT;
        case PEEKED_END_OBJECT:
            return JsonToken.END_OBJECT;
        case PEEKED_BEGIN_ARRAY:
            return JsonToken.BEGIN_ARRAY;
        case PEEKED_END_ARRAY:
            return JsonToken.END_ARRAY;
        case PEEKED_SINGLE_QUOTED_NAME:
        case PEEKED_DOUBLE_QUOTED_NAME:
        case PEEKED_UNQUOTED_NAME:
            return JsonToken.NAME;
        case PEEKED_TRUE:
        case PEEKED_FALSE:
            return JsonToken.BOOLEAN;
        case PEEKED_NULL:
            return JsonToken.NULL;
        case PEEKED_SINGLE_QUOTED:
        case PEEKED_DOUBLE_QUOTED:
        case PEEKED_UNQUOTED:
        case PEEKED_BUFFERED:
            return JsonToken.STRING;
        case PEEKED_LONG:
        case PEEKED_NUMBER:
            return JsonToken.NUMBER;
        case PEEKED_EOF:
            return JsonToken.END_DOCUMENT;
        default:
            throw new AssertionError();
        }
    }

    private int doPeek() {
        int peekStack = stack.get(stackSize - 1);
        if (peekStack == JsonScope.EMPTY_ARRAY) {
            stack.set(stackSize - 1, JsonScope.NONEMPTY_ARRAY);
        } else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
            // Look for a comma before the next element.
            int c = nextNonWhitespace(true);
            switch (c) {
            case ']':
                return peeked = PEEKED_END_ARRAY;
            case ';':
                checkLenient(); // fall-through
            case ',':
                break;
            default:
                throw syntaxError("Unterminated array");
            }
        } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) {
            stack.set(stackSize - 1, JsonScope.DANGLING_NAME);
            // Look for a comma before the next element.
            if (peekStack == JsonScope.NONEMPTY_OBJECT) {
                int c = nextNonWhitespace(true);
                switch (c) {
                case '}':
                    return peeked = PEEKED_END_OBJECT;
                case ';':
                    checkLenient(); // fall-through
                case ',':
                    break;
                default:
                    throw syntaxError("Unterminated object");
                }
            }
            int c = nextNonWhitespace(true);
            switch (c) {
            case '"':
                return peeked = PEEKED_DOUBLE_QUOTED_NAME;
            case '\'':
                checkLenient();
                return peeked = PEEKED_SINGLE_QUOTED_NAME;
            case '}':
                if (peekStack != JsonScope.NONEMPTY_OBJECT) {
                    return peeked = PEEKED_END_OBJECT;
                } else {
                    throw syntaxError("Expected name");
                }
            default:
                checkLenient();
                pos--; // Don't consume the first character in an unquoted string.
                if (isLiteral((char) c)) {
                    return peeked = PEEKED_UNQUOTED_NAME;
                } else {
                    throw syntaxError("Expected name");
                }
            }
        } else if (peekStack == JsonScope.DANGLING_NAME) {
            stack.set(stackSize - 1, JsonScope.NONEMPTY_OBJECT);
            // Look for a colon before the value.
            int c = nextNonWhitespace(true);
            switch (c) {
            case ':':
                break;
            case '=':
                checkLenient();
                if (pos < limit && in.charAt(pos) == '>') {
                    pos++;
                }
                break;
            default:
                throw syntaxError("Expected ':'");
            }
        } else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
            if (lenient) {
                consumeNonExecutePrefix();
            }
            stack.set(stackSize - 1, JsonScope.NONEMPTY_DOCUMENT);
        } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
            int c = nextNonWhitespace(false);
            if (c == -1) {
                return peeked = PEEKED_EOF;
            } else {
                checkLenient();
                pos--;
            }
        } else if (peekStack == JsonScope.CLOSED) {
            throw new IllegalStateException("JsonReader is closed");
        }

        int c = nextNonWhitespace(true);
        switch (c) {
        case ']':
            if (peekStack == JsonScope.EMPTY_ARRAY) {
                return peeked = PEEKED_END_ARRAY;
            }
            // fall-through to handle ",]"
        case ';':
        case ',':
            // In lenient mode, a 0-length literal in an array means 'null'.
            if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) {
                checkLenient();
                pos--;
                return peeked = PEEKED_NULL;
            } else {
                throw syntaxError("Unexpected value");
            }
        case '\'':
            checkLenient();
            return peeked = PEEKED_SINGLE_QUOTED;
        case '"':
            if (stackSize == 1) {
                checkLenient();
            }
            return peeked = PEEKED_DOUBLE_QUOTED;
        case '[':
            return peeked = PEEKED_BEGIN_ARRAY;
        case '{':
            return peeked = PEEKED_BEGIN_OBJECT;
        default:
            pos--; // Don't consume the first character in a literal value.
        }

        if (stackSize == 1) {
            checkLenient(); // Top-level value isn't an array or an object.
        }

        int result = peekKeyword();
        if (result != PEEKED_NONE) {
            return result;
        }

        result = peekNumber();
        if (result != PEEKED_NONE) {
            return result;
        }

        if (!isLiteral(in.charAt(pos))) {
            throw syntaxError("Expected value");
        }

        checkLenient();
        return peeked = PEEKED_UNQUOTED;
    }

    private int peekKeyword() {
        // Figure out which keyword we're matching against by its first character.
        char c = in.charAt(pos);
        String keyword;
        String keywordUpper;
        int peeking;
        if (c == 't' || c == 'T') {
            keyword = "true";
            keywordUpper = "TRUE";
            peeking = PEEKED_TRUE;
        } else if (c == 'f' || c == 'F') {
            keyword = "false";
            keywordUpper = "FALSE";
            peeking = PEEKED_FALSE;
        } else if (c == 'n' || c == 'N') {
            keyword = "null";
            keywordUpper = "NULL";
            peeking = PEEKED_NULL;
        } else {
            return PEEKED_NONE;
        }

        // Confirm that chars [1..length) match the keyword.
        int length = keyword.length();
        for (int i = 1; i < length; i++) {
            if (pos + i >= limit) {
                return PEEKED_NONE;
            }
            c = in.charAt(pos + i);
            if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) {
                return PEEKED_NONE;
            }
        }

        if (pos + length < limit && isLiteral(in.charAt(pos + length))) {
            return PEEKED_NONE; // Don't match trues, falsey or nullsoft!
        }

        // We've found the keyword followed either by EOF or by a non-literal character.
        pos += length;
        return peeked = peeking;
    }

    private int peekNumber() {
        long value = 0; // Negative to accommodate Long.MIN_VALUE more easily.
        boolean negative = false;
        boolean fitsInLong = true;
        int last = NUMBER_CHAR_NONE;

        int i = 0;

        charactersOfNumber: for (; true; i++) {
            if (pos + i == limit) {
                break;
            }

            char c = in.charAt(pos + i);
            switch (c) {
            case '-':
                if (last == NUMBER_CHAR_NONE) {
                    negative = true;
                    last = NUMBER_CHAR_SIGN;
                    continue;
                } else if (last == NUMBER_CHAR_EXP_E) {
                    last = NUMBER_CHAR_EXP_SIGN;
                    continue;
                }
                return PEEKED_NONE;

            case '+':
                if (last == NUMBER_CHAR_EXP_E) {
                    last = NUMBER_CHAR_EXP_SIGN;
                    continue;
                }
                return PEEKED_NONE;

            case 'e':
            case 'E':
                if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) {
                    last = NUMBER_CHAR_EXP_E;
                    continue;
                }
                return PEEKED_NONE;

            case '.':
                if (last == NUMBER_CHAR_DIGIT) {
                    last = NUMBER_CHAR_DECIMAL;
                    continue;
                }
                return PEEKED_NONE;

            default:
                if (c < '0' || c > '9') {
                    if (!isLiteral(c)) {
                        break charactersOfNumber;
                    }
                    return PEEKED_NONE;
                }
                if (last == NUMBER_CHAR_SIGN || last == NUMBER_CHAR_NONE) {
                    value = -(c - '0');
                    last = NUMBER_CHAR_DIGIT;
                } else if (last == NUMBER_CHAR_DIGIT) {
                    if (value == 0) {
                        return PEEKED_NONE; // Leading '0' prefix is not allowed (since it could be octal).
                    }
                    long newValue = value * 10 - (c - '0');
                    fitsInLong &= value > MIN_INCOMPLETE_INTEGER
                            || (value == MIN_INCOMPLETE_INTEGER && newValue < value);
                    value = newValue;
                } else if (last == NUMBER_CHAR_DECIMAL) {
                    last = NUMBER_CHAR_FRACTION_DIGIT;
                } else if (last == NUMBER_CHAR_EXP_E || last == NUMBER_CHAR_EXP_SIGN) {
                    last = NUMBER_CHAR_EXP_DIGIT;
                }
            }
        }

        // We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
        if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)) {
            peekedLong = negative ? value : -value;
            pos += i;
            return peeked = PEEKED_LONG;
        } else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT
                || last == NUMBER_CHAR_EXP_DIGIT) {
            peekedNumberLength = i;
            return peeked = PEEKED_NUMBER;
        } else {
            return PEEKED_NONE;
        }
    }

    private boolean isLiteral(char c) {
        switch (c) {
        case '/':
        case '\\':
        case ';':
        case '#':
        case '=':
            checkLenient(); // fall-through
        case '{':
        case '}':
        case '[':
        case ']':
        case ':':
        case ',':
        case ' ':
        case '\t':
        case '\f':
        case '\r':
        case '\n':
            return false;
        default:
            return true;
        }
    }

    @Override
    public String nextName() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        String result;
        if (p == PEEKED_UNQUOTED_NAME) {
            result = nextUnquotedValue();
        } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
            result = nextQuotedValue('\'');
        } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
            result = nextQuotedValue('"');
        } else {
            throw new IllegalStateException("Expected a name but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }
        peeked = PEEKED_NONE;
        return result;
    }

    @Override
    public String nextString() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        String result;
        if (p == PEEKED_UNQUOTED) {
            result = nextUnquotedValue();
        } else if (p == PEEKED_SINGLE_QUOTED) {
            result = nextQuotedValue('\'');
        } else if (p == PEEKED_DOUBLE_QUOTED) {
            result = nextQuotedValue('"');
        } else if (p == PEEKED_BUFFERED) {
            result = peekedString;
            peekedString = null;
        } else if (p == PEEKED_LONG) {
            result = Long.toString(peekedLong);
        } else if (p == PEEKED_NUMBER) {
            result = in.substring(pos, pos + peekedNumberLength);
            pos += peekedNumberLength;
        } else {
            throw new IllegalStateException("Expected a string but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }
        peeked = PEEKED_NONE;
        return result;
    }

    @Override
    public boolean nextBoolean() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_TRUE) {
            peeked = PEEKED_NONE;
            return true;
        } else if (p == PEEKED_FALSE) {
            peeked = PEEKED_NONE;
            return false;
        }
        throw new IllegalStateException("Expected a boolean but was " + peek() + " at line " + getLineNumber()
                + " column " + getColumnNumber());
    }

    @Override
    public void nextNull() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }
        if (p == PEEKED_NULL) {
            peeked = PEEKED_NONE;
        } else {
            throw new IllegalStateException("Expected null but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }
    }

    @Override
    public double nextDouble() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        if (p == PEEKED_LONG) {
            peeked = PEEKED_NONE;
            return (double) peekedLong;
        }

        if (p == PEEKED_NUMBER) {
            peekedString = in.substring(pos, pos + peekedNumberLength);
            pos += peekedNumberLength;
        } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
            peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
        } else if (p == PEEKED_UNQUOTED) {
            peekedString = nextUnquotedValue();
        } else if (p != PEEKED_BUFFERED) {
            throw new IllegalStateException("Expected a double but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }

        peeked = PEEKED_BUFFERED;
        double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
        if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
            throw syntaxError("JSON forbids NaN and infinities: " + result);
        }
        peekedString = null;
        peeked = PEEKED_NONE;
        return result;
    }

    @Override
    public long nextLong() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        if (p == PEEKED_LONG) {
            peeked = PEEKED_NONE;
            return peekedLong;
        }

        if (p == PEEKED_NUMBER) {
            peekedString = in.substring(pos, pos + peekedNumberLength);
            pos += peekedNumberLength;
        } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
            peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
            try {
                long result = Long.parseLong(peekedString);
                peeked = PEEKED_NONE;
                return result;
            } catch (NumberFormatException ignored) {
                // Fall back to parse as a double below.
            }
        } else {
            throw new IllegalStateException("Expected a long but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }

        peeked = PEEKED_BUFFERED;
        double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
        long result = (long) asDouble;
        if (result != asDouble) { // Make sure no precision was lost casting to 'long'.
            throw new NumberFormatException("Expected a long but was " + peekedString + " at line "
                    + getLineNumber() + " column " + getColumnNumber());
        }
        peekedString = null;
        peeked = PEEKED_NONE;
        return result;
    }

    /**
     * Returns the string up to but not including {@code quote}, unescaping any
     * character escape sequences encountered along the way. The opening quote
     * should have already been read. This consumes the closing quote, but does
     * not include it in the returned string.
     *
     * @param quote either ' or ".
     * @throws NumberFormatException if any unicode escape sequences are
     *     malformed.
     */
    private String nextQuotedValue(char quote) {
        // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
        StringBuilder builder = new StringBuilder();
        int p = pos;
        /* the index of the first character not yet appended to the builder. */
        int start = p;
        while (p < limit) {
            int c = in.charAt(p++);

            if (c == quote) {
                pos = p;
                builder.append(in.substring(start, p - 1));
                return builder.toString();
            } else if (c == '\\') {
                pos = p;
                builder.append(in.substring(start, p - 1));
                builder.append(readEscapeCharacter());
                p = pos;
                start = p;
            } else if (c == '\n') {
                lineNumber++;
                lineStart = p;
            }
        }

        throw syntaxError("Unterminated string");
    }

    /**
     * Returns an unquoted value as a string.
     */
    @SuppressWarnings("fallthrough")
    private String nextUnquotedValue() {
        int i = 0;

        findNonLiteralCharacter: for (; pos + i < limit; i++) {
            switch (in.charAt(pos + i)) {
            case '/':
            case '\\':
            case ';':
            case '#':
            case '=':
                checkLenient(); // fall-through
            case '{':
            case '}':
            case '[':
            case ']':
            case ':':
            case ',':
            case ' ':
            case '\t':
            case '\f':
            case '\r':
            case '\n':
                break findNonLiteralCharacter;
            }
        }

        String result = in.substring(pos, pos + i);
        pos += i;
        return result;
    }

    private void skipQuotedValue(char quote) {
        // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
        int p = pos;
        int l = limit;
        /* the index of the first character not yet appended to the builder. */
        while (p < l) {
            int c = in.charAt(p++);
            if (c == quote) {
                pos = p;
                return;
            } else if (c == '\\') {
                pos = p;
                readEscapeCharacter();
                p = pos;
            } else if (c == '\n') {
                lineNumber++;
                lineStart = p;
            }
        }
        throw syntaxError("Unterminated string");
    }

    private void skipUnquotedValue() {
        int i = 0;
        for (; pos + i < limit; i++) {
            switch (in.charAt(pos + i)) {
            case '/':
            case '\\':
            case ';':
            case '#':
            case '=':
                checkLenient(); // fall-through
            case '{':
            case '}':
            case '[':
            case ']':
            case ':':
            case ',':
            case ' ':
            case '\t':
            case '\f':
            case '\r':
            case '\n':
                pos += i;
                return;
            }
        }
        pos += i;
    }

    @Override
    public int nextInt() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        int result;
        if (p == PEEKED_LONG) {
            result = (int) peekedLong;
            if (peekedLong != result) { // Make sure no precision was lost casting to 'int'.
                throw new NumberFormatException("Expected an int but was " + peekedLong + " at line "
                        + getLineNumber() + " column " + getColumnNumber());
            }
            peeked = PEEKED_NONE;
            return result;
        }

        if (p == PEEKED_NUMBER) {
            peekedString = in.substring(pos, pos + peekedNumberLength);
            pos += peekedNumberLength;
        } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
            peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
            try {
                result = Integer.parseInt(peekedString);
                peeked = PEEKED_NONE;
                return result;
            } catch (NumberFormatException ignored) {
                // Fall back to parse as a double below.
            }
        } else {
            throw new IllegalStateException("Expected an int but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }

        peeked = PEEKED_BUFFERED;
        double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
        result = (int) asDouble;
        if (result != asDouble) { // Make sure no precision was lost casting to 'int'.
            throw new NumberFormatException("Expected an int but was " + peekedString + " at line "
                    + getLineNumber() + " column " + getColumnNumber());
        }
        peekedString = null;
        peeked = PEEKED_NONE;
        return result;
    }

    @Override
    public void close() {
        peeked = PEEKED_NONE;
        stack.set(0, JsonScope.CLOSED);
        stackSize = 1;
    }

    @Override
    public void skipValue() {
        int count = 0;
        do {
            int p = peeked;
            if (p == PEEKED_NONE) {
                p = doPeek();
            }

            if (p == PEEKED_BEGIN_ARRAY) {
                push(JsonScope.EMPTY_ARRAY);
                count++;
            } else if (p == PEEKED_BEGIN_OBJECT) {
                push(JsonScope.EMPTY_OBJECT);
                count++;
            } else if (p == PEEKED_END_ARRAY) {
                stackSize--;
                count--;
            } else if (p == PEEKED_END_OBJECT) {
                stackSize--;
                count--;
            } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
                skipUnquotedValue();
            } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
                skipQuotedValue('\'');
            } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
                skipQuotedValue('"');
            } else if (p == PEEKED_NUMBER) {
                pos += peekedNumberLength;
            }
            peeked = PEEKED_NONE;
        } while (count != 0);
    }

    private void push(int newTop) {
        stack.set(stackSize++, newTop);
    }

    @Override
    public int getLineNumber() {
        return lineNumber + 1;
    }

    @Override
    public int getColumnNumber() {
        return pos - lineStart + 1;
    }

    /**
     * Returns the next character in the stream that is neither whitespace nor a
     * part of a comment. When this returns, the returned character is always at
     * {@code buffer[pos-1]}; this means the caller can always push back the
     * returned character by decrementing {@code pos}.
     */
    private int nextNonWhitespace(boolean throwOnEof) {
        /*
         * This code uses ugly local variables 'p' and 'l' representing the 'pos'
         * and 'limit' fields respectively. Using locals rather than fields saves
         * a few field reads for each whitespace character in a pretty-printed
         * document, resulting in a 5% speedup. We need to flush 'p' to its field
         * before any (potentially indirect) call to fillBuffer() and reread both
         * 'p' and 'l' after any (potentially indirect) call to the same method.
         */
        int p = pos;
        while (true) {
            if (p == limit) {
                pos = p;
                break;
            }

            int c = in.charAt(p++);
            if (c == '\n') {
                lineNumber++;
                lineStart = p;
                continue;
            } else if (c == ' ' || c == '\r' || c == '\t') {
                continue;
            }

            if (c == '/') {
                pos = p;
                if (p == limit) {
                    return c;
                }

                checkLenient();
                char peek = in.charAt(pos);
                switch (peek) {
                case '*':
                    // skip a /* c-style comment */
                    pos++;
                    if (!skipTo("*/")) {
                        throw syntaxError("Unterminated comment");
                    }
                    p = pos + 2;
                    continue;

                case '/':
                    // skip a // end-of-line comment
                    pos++;
                    skipToEndOfLine();
                    p = pos;
                    continue;

                default:
                    return c;
                }
            } else if (c == '#') {
                pos = p;
                /*
                 * Skip a # hash end-of-line comment. The JSON RFC doesn't
                 * specify this behaviour, but it's required to parse
                 * existing documents. See http://b/2571423.
                 */
                checkLenient();
                skipToEndOfLine();
                p = pos;
            } else {
                pos = p;
                return c;
            }
        }
        if (throwOnEof) {
            String mess = "End of input at line " + getLineNumber() + " column " + getColumnNumber();
            logger.log(Level.SEVERE, mess);
            throw new JsonDeserializationException(mess);
        } else {
            return -1;
        }
    }

    private void checkLenient() {
        if (!lenient) {
            throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON");
        }
    }

    /**
     * Advances the position until after the next newline character. If the line
     * is terminated by "\r\n", the '\n' must be consumed as whitespace by the
     * caller.
     */
    private void skipToEndOfLine() {
        while (pos < limit) {
            char c = in.charAt(pos++);
            if (c == '\n') {
                lineNumber++;
                lineStart = pos;
                break;
            } else if (c == '\r') {
                break;
            }
        }
    }

    /**
     * @param toFind a string to search for. Must not contain a newline.
     */
    private boolean skipTo(String toFind) {
        outer: for (; pos + toFind.length() <= limit; pos++) {
            if (in.charAt(pos) == '\n') {
                lineNumber++;
                lineStart = pos + 1;
                continue;
            }
            for (int c = 0; c < toFind.length(); c++) {
                if (in.charAt(pos + c) != toFind.charAt(c)) {
                    continue outer;
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public String toString() {
        return "JsonReader at line " + getLineNumber() + " column " + getColumnNumber();
    }

    /**
     * Unescapes the character identified by the character or characters that
     * immediately follow a backslash. The backslash '\' should have already
     * been read. This supports both unicode escapes "u000A" and two-character
     * escapes "\n".
     *
     * @throws NumberFormatException if any unicode escape sequences are
     *     malformed.
     */
    private char readEscapeCharacter() {
        if (pos == limit) {
            throw syntaxError("Unterminated escape sequence");
        }

        char escaped = in.charAt(pos++);
        switch (escaped) {
        case 'u':
            if (pos + 4 > limit) {
                throw syntaxError("Unterminated escape sequence");
            }
            // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16);
            char result = 0;
            for (int i = pos, end = i + 4; i < end; i++) {
                char c = in.charAt(i);
                result <<= 4;
                if (c >= '0' && c <= '9') {
                    result += (c - '0');
                } else if (c >= 'a' && c <= 'f') {
                    result += (c - 'a' + 10);
                } else if (c >= 'A' && c <= 'F') {
                    result += (c - 'A' + 10);
                } else {
                    throw new NumberFormatException("\\u" + in.substring(pos, pos + 4));
                }
            }
            pos += 4;
            return result;

        case 't':
            return '\t';

        case 'b':
            return '\b';

        case 'n':
            return '\n';

        case 'r':
            return '\r';

        case 'f':
            return '\f';

        case '\n':
            lineNumber++;
            lineStart = pos;
            // fall-through

        case '\'':
        case '"':
        case '\\':
        default:
            return escaped;
        }
    }

    /**
     * Throws a new IO exception with the given message and a context snippet
     * with this reader's content.
     */
    private MalformedJsonException syntaxError(String message) {
        String mess = message + " at line " + getLineNumber() + " column " + getColumnNumber();
        logger.log(Level.SEVERE, mess);
        throw new MalformedJsonException(mess);
    }

    /**
     * Consumes the non-execute prefix if it exists.
     */
    private void consumeNonExecutePrefix() {
        // fast forward through the leading whitespace
        nextNonWhitespace(true);
        pos--;

        if (pos + NON_EXECUTE_PREFIX.length > limit) {
            return;
        }

        for (int i = 0; i < NON_EXECUTE_PREFIX.length; i++) {
            if (in.charAt(pos + i) != NON_EXECUTE_PREFIX[i]) {
                return; // not a security token!
            }
        }

        // we consumed a security token!
        pos += NON_EXECUTE_PREFIX.length;
    }

    @Override
    public String getInput() {
        return in;
    }

    @Override
    public String nextValue() {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        if (p == PEEKED_NULL) {
            peeked = PEEKED_NONE;
            return "null";
        }

        // TODO rewrite to avoid using a JsonWriter
        // we should be able to write the tree without escaping/unescaping
        JsonWriter writer = new FastJsonWriter(new StringBuilder());
        writer.setLenient(true);

        int count = 0;
        do {
            p = peeked;
            if (p == PEEKED_NONE) {
                p = doPeek();
            }

            if (p == PEEKED_BEGIN_ARRAY) {
                push(JsonScope.EMPTY_ARRAY);
                count++;
                writer.beginArray();
            } else if (p == PEEKED_BEGIN_OBJECT) {
                push(JsonScope.EMPTY_OBJECT);
                count++;
                writer.beginObject();
            } else if (p == PEEKED_END_ARRAY) {
                stackSize--;
                count--;
                writer.endArray();
            } else if (p == PEEKED_END_OBJECT) {
                stackSize--;
                count--;
                writer.endObject();
            } else if (p == PEEKED_UNQUOTED_NAME) {
                writer.name(nextUnquotedValue());
            } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
                writer.name(nextQuotedValue('\''));
            } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
                writer.name(nextQuotedValue('"'));
            } else if (p == PEEKED_UNQUOTED) {
                writer.value(nextUnquotedValue());
            } else if (p == PEEKED_SINGLE_QUOTED) {
                writer.value(nextQuotedValue('\''));
            } else if (p == PEEKED_DOUBLE_QUOTED) {
                writer.value(nextQuotedValue('"'));
            } else if (p == PEEKED_NUMBER) {
                writer.value(in.substring(pos, pos + peekedNumberLength));
                pos += peekedNumberLength;
            } else if (p == PEEKED_TRUE) {
                writer.value(true);
            } else if (p == PEEKED_FALSE) {
                writer.value(false);
            } else if (p == PEEKED_LONG) {
                writer.value(peekedLong);
            } else if (p == PEEKED_BUFFERED) {
                writer.value(peekedString);
            }
            peeked = PEEKED_NONE;
        } while (count != 0);

        writer.close();
        return writer.getOutput();
    }

    @Override
    public Number nextNumber() {
        // TODO needs better handling for BigInteger and BigDecimal.
        // Use of DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS and USE_BIG_INTEGER_FOR_INTS. See NumberDeserializer of Jackson.

        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        Number result;
        if (p == PEEKED_LONG) {
            if (peekedLong < 0l) {
                if (peekedLong >= MIN_INT_L) {
                    result = (int) peekedLong;
                } else {
                    result = peekedLong;
                }
            } else {
                if (peekedLong <= MAX_INT_L) {
                    result = (int) peekedLong;
                } else {
                    result = peekedLong;
                }
            }
            peeked = PEEKED_NONE;
            return result;
        }

        if (p == PEEKED_NUMBER) {
            peekedString = in.substring(pos, pos + peekedNumberLength);
            pos += peekedNumberLength;
            peeked = PEEKED_BUFFERED;
            result = Double.parseDouble(peekedString);
            peekedString = null;
            peeked = PEEKED_NONE;
            return result;
        }

        if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
            peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
        } else if (p == PEEKED_UNQUOTED) {
            peekedString = nextUnquotedValue();
        } else if (p != PEEKED_BUFFERED) {
            throw new IllegalStateException("Expected a double but was " + peek() + " at line " + getLineNumber()
                    + " column " + getColumnNumber());
        }

        peeked = PEEKED_BUFFERED;
        if (peekedString.contains(".")) {
            // decimal
            double resultDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
            if (!lenient && (Double.isNaN(resultDouble) || Double.isInfinite(resultDouble))) {
                throw syntaxError("JSON forbids NaN and infinities: " + resultDouble);
            }
            result = resultDouble;
        } else {
            int length = peekedString.length();
            if (length <= 9) { // fits in int
                result = Integer.parseInt(peekedString);
            } else if (length <= 18) { // fits in long and potentially int
                long longResult = Long.parseLong(peekedString);
                if (length == 10) { // can fits in int
                    if (longResult < 0l) {
                        if (longResult >= MIN_INT_L) {
                            result = (int) longResult;
                        } else {
                            result = longResult;
                        }
                    } else {
                        if (longResult <= MAX_INT_L) {
                            result = (int) longResult;
                        } else {
                            result = longResult;
                        }
                    }
                } else {
                    result = longResult;
                }
            } else {
                BigInteger bigIntegerResult = new BigInteger(peekedString);
                if (bigIntegerResult.signum() == -1) {
                    if (bigIntegerResult.compareTo(MIN_LONG_BIGINTEGER) >= 0) {
                        result = bigIntegerResult.longValue();
                    } else {
                        result = bigIntegerResult;
                    }
                } else {
                    if (bigIntegerResult.compareTo(MAX_LONG_BIGINTEGER) <= 0) {
                        result = bigIntegerResult.longValue();
                    } else {
                        result = bigIntegerResult;
                    }
                }
            }
        }
        peekedString = null;
        peeked = PEEKED_NONE;
        return result;
    }

    @Override
    public JavaScriptObject nextJavaScriptObject(boolean useSafeEval) {
        int p = peeked;
        if (p == PEEKED_NONE) {
            p = doPeek();
        }

        switch (p) {
        case PEEKED_BEGIN_OBJECT:
        case PEEKED_BEGIN_ARRAY:
            JavaScriptObject result;
            int peekStack = stack.get(stackSize - 1);
            if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
                // start of the document
                String toEval = in;
                result = useSafeEval ? JsonUtils.safeEval(toEval) : JsonUtils.unsafeEval(toEval);
                // we read everything, we move the pointer to the end of the document
                pos = toEval.length();
                peeked = PEEKED_NONE;
            } else {
                String toEval = nextValue();
                result = useSafeEval ? JsonUtils.safeEval(toEval) : JsonUtils.unsafeEval(toEval);
            }
            return result;
        default:
            throw new IllegalStateException("Expected an array or object to evaluate a JavaScriptObject but was "
                    + peek() + " at line " + getLineNumber() + " column " + getColumnNumber());
        }
    }
}
//@formatter:on