diff options
| author | Jesse Wilson <jessewilson@google.com> | 2010-08-03 17:55:09 -0700 |
|---|---|---|
| committer | Jesse Wilson <jessewilson@google.com> | 2010-08-06 10:34:44 -0700 |
| commit | 76d7e20a75d24afb2f5011a7646dbde8aaa52087 (patch) | |
| tree | e2c154665a99c94c94fb8f5bf44d400befb190fa /core/java/android/util/JsonReader.java | |
| parent | fdde0003e662f0a7c3f13efb49169e3e9e568cce (diff) | |
A new streaming JSON API.
Change-Id: Iefa7ee44ddacbe22ea9ebca3165ac59030659bf2
Diffstat (limited to 'core/java/android/util/JsonReader.java')
| -rw-r--r-- | core/java/android/util/JsonReader.java | 1006 |
1 files changed, 1006 insertions, 0 deletions
diff --git a/core/java/android/util/JsonReader.java b/core/java/android/util/JsonReader.java new file mode 100644 index 000000000000..e47be0d6d28c --- /dev/null +++ b/core/java/android/util/JsonReader.java @@ -0,0 +1,1006 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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 android.util; + +import java.io.IOException; +import java.io.Reader; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; + +/** + * 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 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 JSON on Android?", + * "geo": null, + * "user": { + * "name": "android_newb", + * "followers_count": 41 + * } + * }, + * { + * "id": 912345678902, + * "text": "@android_newb just use android.util.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) throws IOException { + * JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8")); + * return readMessagesArray(reader); + * } + * + * public List<Message> readMessagesArray(JsonReader reader) throws IOException { + * List<Message> messages = new ArrayList<Message>(); + * + * reader.beginArray(); + * while (reader.hasNext()) { + * messages.add(readMessage(reader)); + * } + * reader.endArray(); + * return messages; + * } + * + * public Message readMessage(JsonReader reader) throws IOException { + * 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) throws IOException { + * List<Double> doubles = new ArrayList<Double>(); + * + * reader.beginArray(); + * while (reader.hasNext()) { + * doubles.add(reader.nextDouble()); + * } + * reader.endArray(); + * return doubles; + * } + * + * public User readUser(JsonReader reader) throws IOException { + * 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. + * + * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances + * of this class are not thread safe. + */ +public final class JsonReader implements Closeable { + + /** The input JSON. */ + private final Reader in; + + /** + * Use a manual buffer to easily read and unread upcoming characters, and + * also so we can create strings without an intermediate StringBuilder. + */ + private final char[] buffer = new char[1024]; + private int pos = 0; + private int limit = 0; + + private final List<JsonScope> stack = new ArrayList<JsonScope>(); + { + push(JsonScope.EMPTY_DOCUMENT); + } + + /** + * True if we've already read the next token. If we have, the string value + * for that token will be assigned to {@code value} if such a string value + * exists. And the token type will be assigned to {@code token} if the token + * type is known. The token type may be null for literals, since we derive + * that lazily. + */ + private boolean hasToken; + + /** + * The type of the next token to be returned by {@link #peek} and {@link + * #advance}, or {@code null} if it is unknown and must first be derived + * from {@code value}. This value is undefined if {@code hasToken} is false. + */ + private JsonToken token; + + /** The text of the next name. */ + private String name; + + /** The text of the next literal value. */ + private String value; + + // TODO: make this parser strict and offer an optional lenient mode? + // TODO: document how this reader is non-strict + + /** + * Creates a new instance that reads a JSON-encoded stream from {@code in}. + */ + public JsonReader(Reader in) { + if (in == null) { + throw new NullPointerException("in == null"); + } + this.in = in; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new array. + */ + public void beginArray() throws IOException { + expect(JsonToken.BEGIN_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endArray() throws IOException { + expect(JsonToken.END_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new object. + */ + public void beginObject() throws IOException { + expect(JsonToken.BEGIN_OBJECT); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endObject() throws IOException { + expect(JsonToken.END_OBJECT); + } + + /** + * Consumes {@code token}. + */ + private void expect(JsonToken token) throws IOException { + if (quickPeek() != token) { + throw new IllegalStateException("Expected " + token + " but was " + peek()); + } + advance(); + } + + /** + * Returns true if the current array or object has another element. + */ + public boolean hasNext() throws IOException { + JsonToken peek = quickPeek(); + return peek != JsonToken.END_OBJECT && peek != JsonToken.END_ARRAY; + } + + /** + * Returns the type of the next token without consuming it. + */ + public JsonToken peek() throws IOException { + quickPeek(); + + if (token == null) { + decodeLiteral(); + } + + return token; + } + + /** + * Ensures that a token is ready. After this call either {@code token} or + * {@code value} will be non-null. + * + * @return the type of the next token, of {@code null} if it is unknown. For + * a definitive result, use {@link #peek()} which decodes the token + * type. + */ + private JsonToken quickPeek() throws IOException { + if (hasToken) { + return token; + } + + switch (peekStack()) { + case EMPTY_DOCUMENT: + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + JsonToken firstToken = nextValue(); + if (token != JsonToken.BEGIN_ARRAY && token != JsonToken.BEGIN_OBJECT) { + throw new IOException( + "Expected JSON document to start with '[' or '{' but was " + token); + } + return firstToken; + case EMPTY_ARRAY: + return nextInArray(true); + case NONEMPTY_ARRAY: + return nextInArray(false); + case EMPTY_OBJECT: + return nextInObject(true); + case DANGLING_NAME: + return objectValue(); + case NONEMPTY_OBJECT: + return nextInObject(false); + case NONEMPTY_DOCUMENT: + hasToken = true; + return token = JsonToken.END_DOCUMENT; + case CLOSED: + throw new IllegalStateException("JsonReader is closed"); + default: + throw new AssertionError(); + } + } + + /** + * Advances the cursor in the JSON stream to the next token. + */ + private JsonToken advance() throws IOException { + quickPeek(); + + JsonToken result = token; + hasToken = false; + token = null; + value = null; + name = null; + return result; + } + + /** + * Returns the next token, a {@link JsonToken#NAME property name}, and + * consumes it. + * + * @throws IOException if the next token in the stream is not a property + * name. + */ + public String nextName() throws IOException { + if (quickPeek() != JsonToken.NAME) { + throw new IllegalStateException("Expected a name but was " + peek()); + } + String result = name; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#STRING string} value of the next token, + * consuming it. If the next token is a number, this method will return its + * string form. + * + * @throws IllegalStateException if the next token is not a string or if + * this reader is closed. + */ + public String nextString() throws IOException { + JsonToken peek = peek(); + if (value == null || (peek != JsonToken.STRING && peek != JsonToken.NUMBER)) { + throw new IllegalStateException("Expected a string but was " + peek()); + } + + String result = value; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, + * consuming it. + * + * @throws IllegalStateException if the next token is not a boolean or if + * this reader is closed. + */ + public boolean nextBoolean() throws IOException { + JsonToken peek = quickPeek(); + if (value == null || peek == JsonToken.STRING) { + throw new IllegalStateException("Expected a boolean but was " + peek()); + } + + boolean result; + if (value.equalsIgnoreCase("true")) { + result = true; + } else if (value.equalsIgnoreCase("false")) { + result = false; + } else { + throw new IllegalStateException("Not a boolean: " + value); + } + + advance(); + return result; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is a + * literal null. + * + * @throws IllegalStateException if the next token is not null or if this + * reader is closed. + */ + public void nextNull() throws IOException { + JsonToken peek = quickPeek(); + if (value == null || peek == JsonToken.STRING) { + throw new IllegalStateException("Expected null but was " + peek()); + } + + if (!value.equalsIgnoreCase("null")) { + throw new IllegalStateException("Not a null: " + value); + } + + advance(); + } + + /** + * Returns the {@link JsonToken#NUMBER double} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a double. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a double, or is non-finite. + */ + public double nextDouble() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected a double but was " + peek()); + } + + double result = Double.parseDouble(value); + + if ((result >= 1.0d && value.startsWith("0")) + || Double.isNaN(result) + || Double.isInfinite(result)) { + throw new NumberFormatException( + "JSON forbids octal prefixes, NaN and infinities: " + value); + } + + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER long} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a long. If the next token's numeric value cannot be exactly + * represented by a Java {@code long}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as a long. + */ + public long nextLong() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected a long but was " + peek()); + } + + long result; + try { + result = Long.parseLong(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (long) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + if (result >= 1L && value.startsWith("0")) { + throw new NumberFormatException("JSON forbids octal prefixes: " + value); + } + + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER int} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as an int. If the next token's numeric value cannot be exactly + * represented by a Java {@code int}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as an int. + */ + public int nextInt() throws IOException { + quickPeek(); + if (value == null) { + throw new IllegalStateException("Expected an int but was " + peek()); + } + + int result; + try { + result = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (int) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + if (result >= 1L && value.startsWith("0")) { + throw new NumberFormatException("JSON forbids octal prefixes: " + value); + } + + advance(); + return result; + } + + /** + * Closes this JSON reader and the underlying {@link Reader}. + */ + public void close() throws IOException { + hasToken = false; + value = null; + token = null; + stack.clear(); + stack.add(JsonScope.CLOSED); + in.close(); + } + + /** + * Skips the next value recursively. If it is an object or array, all nested + * elements are skipped. This method is intended for use when the JSON token + * stream contains unrecognized or unhandled values. + */ + public void skipValue() throws IOException { + // TODO: suppress string creation while elements are being skipped! + + int count = 0; + do { + JsonToken token = advance(); + if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) { + count++; + } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) { + count--; + } + } while (count != 0); + } + + private JsonScope peekStack() { + return stack.get(stack.size() - 1); + } + + private JsonScope pop() { + return stack.remove(stack.size() - 1); + } + + private void push(JsonScope newTop) { + stack.add(newTop); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(JsonScope newTop) { + stack.set(stack.size() - 1, newTop); + } + + private JsonToken nextInArray(boolean firstElement) throws IOException { + if (firstElement) { + switch (nextNonWhitespace()) { + case ']': + pop(); + hasToken = true; + return token = JsonToken.END_ARRAY; + case ',': + case ';': + /* a separator without a value first means "null". */ + // TODO: forbid this in strict mode + hasToken = true; + return token = JsonToken.NULL; + default: + replaceTop(JsonScope.NONEMPTY_ARRAY); + pos--; + } + } else { + switch (nextNonWhitespace()) { + case ']': + pop(); + hasToken = true; + return token = JsonToken.END_ARRAY; + case ',': + case ';': + break; + default: + throw syntaxError("Unterminated array"); + } + } + + return nextValue(); + } + + private JsonToken nextInObject(boolean firstElement) throws IOException { + /* + * Read delimiters. Either a comma/semicolon separating this and the + * previous name-value pair, or a close brace to denote the end of the + * object. + */ + if (firstElement) { + /* Peek to see if this is the empty object. */ + switch (nextNonWhitespace()) { + case '}': + pop(); + hasToken = true; + return token = JsonToken.END_OBJECT; + default: + pos--; + } + } else { + switch (nextNonWhitespace()) { + case '}': + pop(); + hasToken = true; + return token = JsonToken.END_OBJECT; + case ';': + case ',': + break; + default: + throw syntaxError("Unterminated object"); + } + } + + /* Read the name. */ + int quote = nextNonWhitespace(); + switch (quote) { + case '\'': + case '"': + name = nextString((char) quote); + break; + default: + pos--; + name = nextLiteral(); + if (name.isEmpty()) { + throw syntaxError("Expected name"); + } + } + + replaceTop(JsonScope.DANGLING_NAME); + hasToken = true; + return token = JsonToken.NAME; + } + + private JsonToken objectValue() throws IOException { + // TODO: accept only ":" in strict mode + + /* + * Read the name/value separator. Usually a colon ':', an equals sign + * '=', or an arrow "=>". The last two are bogus but we include them + * because that's what org.json does. + */ + int separator = nextNonWhitespace(); + if (separator != ':' && separator != '=') { + throw syntaxError("Expected ':'"); + } + if (separator == '=' && (pos < limit || fillBuffer(1)) && buffer[pos] == '>') { + pos++; + } + + replaceTop(JsonScope.NONEMPTY_OBJECT); + return nextValue(); + } + + private JsonToken nextValue() throws IOException { + int c = nextNonWhitespace(); + switch (c) { + case '{': + push(JsonScope.EMPTY_OBJECT); + hasToken = true; + return token = JsonToken.BEGIN_OBJECT; + + case '[': + push(JsonScope.EMPTY_ARRAY); + hasToken = true; + return token = JsonToken.BEGIN_ARRAY; + + case '\'': + case '"': + value = nextString((char) c); + hasToken = true; + return token = JsonToken.STRING; + + default: + pos--; + return readLiteral(); + } + } + + /** + * Returns true once {@code limit - pos >= minimum}. If the data is + * exhausted before that many characters are available, this returns + * false. + */ + private boolean fillBuffer(int minimum) throws IOException { + if (limit != pos) { + limit -= pos; + System.arraycopy(buffer, pos, buffer, 0, limit); + } else { + limit = 0; + } + + pos = 0; + int total; + while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) { + limit += total; + if (limit >= minimum) { + return true; + } + } + return false; + } + + private int nextNonWhitespace() throws IOException { + // TODO: no comments in strict mode + + while (pos < limit || fillBuffer(1)) { + int c = buffer[pos++]; + switch (c) { + case '\t': + case ' ': + case '\n': + case '\r': + continue; + + case '/': + if (pos == limit && !fillBuffer(1)) { + return c; + } + + char peek = buffer[pos]; + switch (peek) { + case '*': + // skip a /* c-style comment */ + pos++; + if (!skipTo("*/")) { + throw syntaxError("Unterminated comment"); + } + pos += 2; + continue; + + case '/': + // skip a // end-of-line comment + pos++; + skipToEndOfLine(); + continue; + + default: + return c; + } + + case '#': + /* + * 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. + */ + skipToEndOfLine(); + continue; + + default: + return c; + } + } + + throw syntaxError("End of input"); + } + + /** + * 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() throws IOException { + while (pos < limit || fillBuffer(1)) { + char c = buffer[pos++]; + if (c == '\r' || c == '\n') { + break; + } + } + } + + private boolean skipTo(String toFind) throws IOException { + outer: + for (; pos + toFind.length() < limit || fillBuffer(toFind.length()); pos++) { + for (int c = 0; c < toFind.length(); c++) { + if (buffer[pos + c] != toFind.charAt(c)) { + continue outer; + } + } + return true; + } + return false; + } + + /** + * 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 nextString(char quote) throws IOException { + StringBuilder builder = null; + do { + /* the index of the first character not yet appended to the builder. */ + int start = pos; + while (pos < limit) { + int c = buffer[pos++]; + + if (c == quote) { + if (builder == null) { + return new String(buffer, start, pos - start - 1); + } else { + builder.append(buffer, start, pos - start - 1); + return builder.toString(); + } + + } else if (c == '\\') { + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start - 1); + builder.append(readEscapeCharacter()); + start = pos; + } + } + + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start); + } while (fillBuffer(1)); + + throw syntaxError("Unterminated string"); + } + + /** + * Returns the string up to but not including any delimiter characters. This + * does not consume the delimiter character. + */ + private String nextLiteral() throws IOException { + // TODO: use a much smaller set of permitted literal characters in strict mode; + // these characters are derived from org.json's lenient mode + + StringBuilder builder = null; + do { + /* the index of the first character not yet appended to the builder. */ + int start = pos; + while (pos < limit) { + int c = buffer[pos++]; + switch (c) { + case '{': + case '}': + case '[': + case ']': + case '/': + case '\\': + case ':': + case '=': + case ',': + case ';': + case '#': + case ' ': + case '\t': + case '\f': + case '\r': + case '\n': + pos--; + if (builder == null) { + return new String(buffer, start, pos - start); + } else { + builder.append(buffer, start, pos - start); + return builder.toString(); + } + } + } + + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start); + } while (fillBuffer(1)); + + return builder.toString(); + } + + @Override public String toString() { + return getClass().getSimpleName() + " near " + getSnippet(); + } + + /** + * 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() throws IOException { + if (pos == limit && !fillBuffer(1)) { + throw syntaxError("Unterminated escape sequence"); + } + + char escaped = buffer[pos++]; + switch (escaped) { + case 'u': + if (pos + 4 > limit && !fillBuffer(4)) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = new String(buffer, pos, 4); + pos += 4; + return (char) Integer.parseInt(hex, 16); + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\'': + case '"': + case '\\': + default: + return escaped; + } + } + + /** + * Reads a null, boolean, numeric or unquoted string literal value. + */ + private JsonToken readLiteral() throws IOException { + String literal = nextLiteral(); + if (literal.isEmpty()) { + throw syntaxError("Expected literal value"); + } + value = literal; + hasToken = true; + return token = null; // use decodeLiteral() to get the token type + } + + /** + * Assigns {@code nextToken} based on the value of {@code nextValue}. + */ + private void decodeLiteral() { + if (value.equalsIgnoreCase("null")) { + token = JsonToken.NULL; + } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + token = JsonToken.BOOLEAN; + } else { + try { + Double.parseDouble(value); // this work could potentially be cached + token = JsonToken.NUMBER; + } catch (NumberFormatException ignored) { + /* an unquoted string. This document is not well-formed! */ + token = JsonToken.STRING; + } + } + } + + /** + * Throws a new IO exception with the given message and a context snippet + * with this reader's content. + */ + public IOException syntaxError(String message) throws IOException { + throw new JsonSyntaxException(message + " near " + getSnippet()); + } + + private CharSequence getSnippet() { + StringBuilder snippet = new StringBuilder(); + int beforePos = Math.min(pos, 20); + snippet.append(buffer, pos - beforePos, beforePos); + int afterPos = Math.min(limit - pos, 20); + snippet.append(buffer, pos, afterPos); + return snippet; + } + + private static class JsonSyntaxException extends IOException { + private JsonSyntaxException(String s) { + super(s); + } + } +} |
