diff --git a/src/api-impl/android/util/JsonReader.java b/src/api-impl/android/util/JsonReader.java new file mode 100644 index 00000000..40a2db70 --- /dev/null +++ b/src/api-impl/android/util/JsonReader.java @@ -0,0 +1,1173 @@ +/* + * 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.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; + +import libcore.internal.StringPool; + + +/** + * Reads a JSON (RFC 4627) + * 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. + * + *
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. + *
When a nested object or array is encountered, delegate to the + * corresponding handler method. + * + *
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. + * + *
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()}. + * + *
{@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
+ * }
+ * }
+ * ]}
+ * This code implements the parser for the above structure: {@code
+ *
+ * public List readJsonStream(InputStream in) throws IOException {
+ * JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
+ * try {
+ * return readMessagesArray(reader);
+ * } finally {
+ * reader.close();
+ * }
+ * }
+ *
+ * public List readMessagesArray(JsonReader reader) throws IOException {
+ * List messages = new ArrayList();
+ *
+ * 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 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 readDoublesArray(JsonReader reader) throws IOException {
+ * List doubles = new ArrayList();
+ *
+ * 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);
+ * }}
+ *
+ * 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 {
+
+ private static final String TRUE = "true";
+ private static final String FALSE = "false";
+
+ private final StringPool stringPool = new StringPool();
+
+ /** The input JSON. */
+ private final Reader in;
+
+ /** True to accept non-spec compliant JSON */
+ private boolean lenient = false;
+
+ /**
+ * Use a manual buffer to easily read and unread upcoming characters, and
+ * also so we can create strings without an intermediate StringBuilder.
+ * We decode literals directly out of this buffer, so it must be at least as
+ * long as the longest token that can be reported as a number.
+ */
+ private final char[] buffer = new char[1024];
+ private int pos = 0;
+ private int limit = 0;
+
+ /*
+ * The offset of the first character in the buffer.
+ */
+ private int bufferStartLine = 1;
+ private int bufferStartColumn = 1;
+
+ private final List Each {@code JsonWriter} may be used to write a single JSON stream.
+ * Instances of this class are not thread safe. Calls that would result in a
+ * malformed JSON string will fail with an {@link IllegalStateException}.
+ */
+public final class JsonWriter implements Closeable {
+
+ /** The output data, containing at most one top-level array or object. */
+ private final Writer out;
+
+ private final List
+ *
+ */
+ public void setLenient(boolean lenient) {
+ this.lenient = lenient;
+ }
+
+ /**
+ * Returns true if this parser is liberal in what it accepts.
+ */
+ public boolean isLenient() {
+ return lenient;
+ }
+
+ /**
+ * 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 object.
+ */
+ public void endObject() throws IOException {
+ expect(JsonToken.END_OBJECT);
+ }
+
+ /**
+ * Consumes {@code expected}.
+ */
+ private void expect(JsonToken expected) throws IOException {
+ peek();
+ if (token != expected) {
+ throw new IllegalStateException("Expected " + expected + " but was " + peek());
+ }
+ advance();
+ }
+
+ /**
+ * Returns true if the current array or object has another element.
+ */
+ public boolean hasNext() throws IOException {
+ peek();
+ return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY;
+ }
+
+ /**
+ * Returns the type of the next token without consuming it.
+ */
+ public JsonToken peek() throws IOException {
+ if (token != null) {
+ return token;
+ }
+
+ switch (peekStack()) {
+ case EMPTY_DOCUMENT:
+ replaceTop(JsonScope.NONEMPTY_DOCUMENT);
+ JsonToken firstToken = nextValue();
+ if (!lenient && 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:
+ try {
+ JsonToken token = nextValue();
+ if (lenient) {
+ return token;
+ }
+ throw syntaxError("Expected EOF");
+ } catch (EOFException e) {
+ return token = JsonToken.END_DOCUMENT; // TODO: avoid throwing here?
+ }
+ 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 {
+ peek();
+
+ JsonToken result = token;
+ 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 {
+ peek();
+ if (token != 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 {
+ peek();
+ if (token != JsonToken.STRING && token != 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 {
+ peek();
+ if (token != JsonToken.BOOLEAN) {
+ throw new IllegalStateException("Expected a boolean but was " + token);
+ }
+
+ boolean result = (value == TRUE);
+ 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 {
+ peek();
+ if (token != JsonToken.NULL) {
+ throw new IllegalStateException("Expected null but was " + token);
+ }
+
+ 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 using {@link Double#parseDouble(String)}.
+ *
+ * @throws IllegalStateException if the next token is not a literal value.
+ */
+ public double nextDouble() throws IOException {
+ peek();
+ if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+ throw new IllegalStateException("Expected a double but was " + token);
+ }
+
+ double result = Double.parseDouble(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 {
+ peek();
+ if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+ throw new IllegalStateException("Expected a long but was " + token);
+ }
+
+ 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);
+ }
+ }
+
+ 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 {
+ peek();
+ if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+ throw new IllegalStateException("Expected an int but was " + token);
+ }
+
+ 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);
+ }
+ }
+
+ advance();
+ return result;
+ }
+
+ /**
+ * Closes this JSON reader and the underlying {@link Reader}.
+ */
+ public void close() throws IOException {
+ 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 {
+ skipping = true;
+ try {
+ if (!hasNext() || peek() == JsonToken.END_DOCUMENT) {
+ throw new IllegalStateException("No element left to skip");
+ }
+ 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);
+ } finally {
+ skipping = false;
+ }
+ }
+
+ 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) {
+ replaceTop(JsonScope.NONEMPTY_ARRAY);
+ } else {
+ /* Look for a comma before each element after the first element. */
+ switch (nextNonWhitespace()) {
+ case ']':
+ pop();
+ return token = JsonToken.END_ARRAY;
+ case ';':
+ checkLenient(); // fall-through
+ case ',':
+ break;
+ default:
+ throw syntaxError("Unterminated array");
+ }
+ }
+
+ switch (nextNonWhitespace()) {
+ case ']':
+ if (firstElement) {
+ pop();
+ return token = JsonToken.END_ARRAY;
+ }
+ // fall-through to handle ",]"
+ case ';':
+ case ',':
+ /* In lenient mode, a 0-length literal means 'null' */
+ checkLenient();
+ pos--;
+ value = "null";
+ return token = JsonToken.NULL;
+ default:
+ pos--;
+ 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();
+ return token = JsonToken.END_OBJECT;
+ default:
+ pos--;
+ }
+ } else {
+ switch (nextNonWhitespace()) {
+ case '}':
+ pop();
+ return token = JsonToken.END_OBJECT;
+ case ';':
+ case ',':
+ break;
+ default:
+ throw syntaxError("Unterminated object");
+ }
+ }
+
+ /* Read the name. */
+ int quote = nextNonWhitespace();
+ switch (quote) {
+ case '\'':
+ checkLenient(); // fall-through
+ case '"':
+ name = nextString((char) quote);
+ break;
+ default:
+ checkLenient();
+ pos--;
+ name = nextLiteral(false);
+ if (name.isEmpty()) {
+ throw syntaxError("Expected name");
+ }
+ }
+
+ replaceTop(JsonScope.DANGLING_NAME);
+ return token = JsonToken.NAME;
+ }
+
+ private JsonToken objectValue() throws IOException {
+ /*
+ * Read the name/value separator. Usually a colon ':'. In lenient mode
+ * we also accept an equals sign '=', or an arrow "=>".
+ */
+ switch (nextNonWhitespace()) {
+ case ':':
+ break;
+ case '=':
+ checkLenient();
+ if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
+ pos++;
+ }
+ break;
+ default:
+ throw syntaxError("Expected ':'");
+ }
+
+ replaceTop(JsonScope.NONEMPTY_OBJECT);
+ return nextValue();
+ }
+
+ private JsonToken nextValue() throws IOException {
+ int c = nextNonWhitespace();
+ switch (c) {
+ case '{':
+ push(JsonScope.EMPTY_OBJECT);
+ return token = JsonToken.BEGIN_OBJECT;
+
+ case '[':
+ push(JsonScope.EMPTY_ARRAY);
+ return token = JsonToken.BEGIN_ARRAY;
+
+ case '\'':
+ checkLenient(); // fall-through
+ case '"':
+ value = nextString((char) c);
+ 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 {
+ // Before clobbering the old characters, update where buffer starts
+ for (int i = 0; i < pos; i++) {
+ if (buffer[i] == '\n') {
+ bufferStartLine++;
+ bufferStartColumn = 1;
+ } else {
+ bufferStartColumn++;
+ }
+ }
+
+ 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 this is the first read, consume an optional byte order mark (BOM) if it exists
+ if (bufferStartLine == 1 && bufferStartColumn == 1
+ && limit > 0 && buffer[0] == '\ufeff') {
+ pos++;
+ bufferStartColumn--;
+ }
+
+ if (limit >= minimum) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private int getLineNumber() {
+ int result = bufferStartLine;
+ for (int i = 0; i < pos; i++) {
+ if (buffer[i] == '\n') {
+ result++;
+ }
+ }
+ return result;
+ }
+
+ private int getColumnNumber() {
+ int result = bufferStartColumn;
+ for (int i = 0; i < pos; i++) {
+ if (buffer[i] == '\n') {
+ result = 1;
+ } else {
+ result++;
+ }
+ }
+ return result;
+ }
+
+ private int nextNonWhitespace() throws IOException {
+ 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;
+ }
+
+ checkLenient();
+ 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.
+ */
+ checkLenient();
+ skipToEndOfLine();
+ continue;
+
+ default:
+ return c;
+ }
+ }
+
+ throw new EOFException("End of input");
+ }
+
+ private void checkLenient() throws IOException {
+ 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() 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 (skipping) {
+ return "skipped!";
+ } else if (builder == null) {
+ return stringPool.get(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");
+ }
+
+ /**
+ * Reads the value up to but not including any delimiter characters. This
+ * does not consume the delimiter character.
+ *
+ * @param assignOffsetsOnly true for this method to only set the valuePos
+ * and valueLength fields and return a null result. This only works if
+ * the literal is short; a string is returned otherwise.
+ */
+ private String nextLiteral(boolean assignOffsetsOnly) throws IOException {
+ StringBuilder builder = null;
+ valuePos = -1;
+ valueLength = 0;
+ int i = 0;
+
+ findNonLiteralCharacter:
+ while (true) {
+ for (; pos + i < limit; i++) {
+ switch (buffer[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;
+ }
+ }
+
+ /*
+ * Attempt to load the entire literal into the buffer at once. If
+ * we run out of input, add a non-literal character at the end so
+ * that decoding doesn't need to do bounds checks.
+ */
+ if (i < buffer.length) {
+ if (fillBuffer(i + 1)) {
+ continue;
+ } else {
+ buffer[limit] = '\0';
+ break;
+ }
+ }
+
+ // use a StringBuilder when the value is too long. It must be an unquoted string.
+ if (builder == null) {
+ builder = new StringBuilder();
+ }
+ builder.append(buffer, pos, i);
+ valueLength += i;
+ pos += i;
+ i = 0;
+ if (!fillBuffer(1)) {
+ break;
+ }
+ }
+
+ String result;
+ if (assignOffsetsOnly && builder == null) {
+ valuePos = pos;
+ result = null;
+ } else if (skipping) {
+ result = "skipped!";
+ } else if (builder == null) {
+ result = stringPool.get(buffer, pos, i);
+ } else {
+ builder.append(buffer, pos, i);
+ result = builder.toString();
+ }
+ valueLength += i;
+ pos += i;
+ return result;
+ }
+
+ @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 = stringPool.get(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 {
+ value = nextLiteral(true);
+ if (valueLength == 0) {
+ throw syntaxError("Expected literal value");
+ }
+ token = decodeLiteral();
+ if (token == JsonToken.STRING) {
+ checkLenient();
+ }
+ return token;
+ }
+
+ /**
+ * Assigns {@code nextToken} based on the value of {@code nextValue}.
+ */
+ private JsonToken decodeLiteral() throws IOException {
+ if (valuePos == -1) {
+ // it was too long to fit in the buffer so it can only be a string
+ return JsonToken.STRING;
+ } else if (valueLength == 4
+ && ('n' == buffer[valuePos ] || 'N' == buffer[valuePos ])
+ && ('u' == buffer[valuePos + 1] || 'U' == buffer[valuePos + 1])
+ && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2])
+ && ('l' == buffer[valuePos + 3] || 'L' == buffer[valuePos + 3])) {
+ value = "null";
+ return JsonToken.NULL;
+ } else if (valueLength == 4
+ && ('t' == buffer[valuePos ] || 'T' == buffer[valuePos ])
+ && ('r' == buffer[valuePos + 1] || 'R' == buffer[valuePos + 1])
+ && ('u' == buffer[valuePos + 2] || 'U' == buffer[valuePos + 2])
+ && ('e' == buffer[valuePos + 3] || 'E' == buffer[valuePos + 3])) {
+ value = TRUE;
+ return JsonToken.BOOLEAN;
+ } else if (valueLength == 5
+ && ('f' == buffer[valuePos ] || 'F' == buffer[valuePos ])
+ && ('a' == buffer[valuePos + 1] || 'A' == buffer[valuePos + 1])
+ && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2])
+ && ('s' == buffer[valuePos + 3] || 'S' == buffer[valuePos + 3])
+ && ('e' == buffer[valuePos + 4] || 'E' == buffer[valuePos + 4])) {
+ value = FALSE;
+ return JsonToken.BOOLEAN;
+ } else {
+ value = stringPool.get(buffer, valuePos, valueLength);
+ return decodeNumber(buffer, valuePos, valueLength);
+ }
+ }
+
+ /**
+ * Determine whether the characters is a JSON number. Numbers are of the
+ * form -12.34e+56. Fractional and exponential parts are optional. Leading
+ * zeroes are not allowed in the value or exponential part, but are allowed
+ * in the fraction.
+ */
+ private JsonToken decodeNumber(char[] chars, int offset, int length) {
+ int i = offset;
+ int c = chars[i];
+
+ if (c == '-') {
+ c = chars[++i];
+ }
+
+ if (c == '0') {
+ c = chars[++i];
+ } else if (c >= '1' && c <= '9') {
+ c = chars[++i];
+ while (c >= '0' && c <= '9') {
+ c = chars[++i];
+ }
+ } else {
+ return JsonToken.STRING;
+ }
+
+ if (c == '.') {
+ c = chars[++i];
+ while (c >= '0' && c <= '9') {
+ c = chars[++i];
+ }
+ }
+
+ if (c == 'e' || c == 'E') {
+ c = chars[++i];
+ if (c == '+' || c == '-') {
+ c = chars[++i];
+ }
+ if (c >= '0' && c <= '9') {
+ c = chars[++i];
+ while (c >= '0' && c <= '9') {
+ c = chars[++i];
+ }
+ } else {
+ return JsonToken.STRING;
+ }
+ }
+
+ if (i == offset + length) {
+ return JsonToken.NUMBER;
+ } else {
+ return JsonToken.STRING;
+ }
+ }
+
+ /**
+ * Throws a new IO exception with the given message and a context snippet
+ * with this reader's content.
+ */
+ private IOException syntaxError(String message) throws IOException {
+ throw new IOException(message
+ + " at line " + getLineNumber() + " column " + getColumnNumber());
+ }
+
+ 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;
+ }
+}
diff --git a/src/api-impl/android/util/JsonScope.java b/src/api-impl/android/util/JsonScope.java
new file mode 100644
index 00000000..02c83629
--- /dev/null
+++ b/src/api-impl/android/util/JsonScope.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+/**
+ * Lexical scoping elements within a JSON reader or writer.
+ */
+enum JsonScope {
+
+ /**
+ * An array with no elements requires no separators or newlines before
+ * it is closed.
+ */
+ EMPTY_ARRAY,
+
+ /**
+ * A array with at least one value requires a comma and newline before
+ * the next element.
+ */
+ NONEMPTY_ARRAY,
+
+ /**
+ * An object with no name/value pairs requires no separators or newlines
+ * before it is closed.
+ */
+ EMPTY_OBJECT,
+
+ /**
+ * An object whose most recent element is a key. The next element must
+ * be a value.
+ */
+ DANGLING_NAME,
+
+ /**
+ * An object with at least one name/value pair requires a comma and
+ * newline before the next element.
+ */
+ NONEMPTY_OBJECT,
+
+ /**
+ * No object or array has been started.
+ */
+ EMPTY_DOCUMENT,
+
+ /**
+ * A document with at an array or object.
+ */
+ NONEMPTY_DOCUMENT,
+
+ /**
+ * A document that's been closed and cannot be accessed.
+ */
+ CLOSED,
+}
diff --git a/src/api-impl/android/util/JsonToken.java b/src/api-impl/android/util/JsonToken.java
new file mode 100644
index 00000000..1e6c27b3
--- /dev/null
+++ b/src/api-impl/android/util/JsonToken.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+/**
+ * A structure, name or value type in a JSON-encoded string.
+ */
+public enum JsonToken {
+
+ /**
+ * The opening of a JSON array. Written using {@link JsonWriter#beginObject}
+ * and read using {@link JsonReader#beginObject}.
+ */
+ BEGIN_ARRAY,
+
+ /**
+ * The closing of a JSON array. Written using {@link JsonWriter#endArray}
+ * and read using {@link JsonReader#endArray}.
+ */
+ END_ARRAY,
+
+ /**
+ * The opening of a JSON object. Written using {@link JsonWriter#beginObject}
+ * and read using {@link JsonReader#beginObject}.
+ */
+ BEGIN_OBJECT,
+
+ /**
+ * The closing of a JSON object. Written using {@link JsonWriter#endObject}
+ * and read using {@link JsonReader#endObject}.
+ */
+ END_OBJECT,
+
+ /**
+ * A JSON property name. Within objects, tokens alternate between names and
+ * their values. Written using {@link JsonWriter#name} and read using {@link
+ * JsonReader#nextName}
+ */
+ NAME,
+
+ /**
+ * A JSON string.
+ */
+ STRING,
+
+ /**
+ * A JSON number represented in this API by a Java {@code double}, {@code
+ * long}, or {@code int}.
+ */
+ NUMBER,
+
+ /**
+ * A JSON {@code true} or {@code false}.
+ */
+ BOOLEAN,
+
+ /**
+ * A JSON {@code null}.
+ */
+ NULL,
+
+ /**
+ * The end of the JSON stream. This sentinel value is returned by {@link
+ * JsonReader#peek()} to signal that the JSON-encoded value has no more
+ * tokens.
+ */
+ END_DOCUMENT
+}
\ No newline at end of file
diff --git a/src/api-impl/android/util/JsonWriter.java b/src/api-impl/android/util/JsonWriter.java
new file mode 100644
index 00000000..a5cc1325
--- /dev/null
+++ b/src/api-impl/android/util/JsonWriter.java
@@ -0,0 +1,528 @@
+/*
+ * 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.Closeable;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Writes a JSON (RFC 4627)
+ * encoded value to a stream, one token at a time. The stream includes both
+ * literal values (strings, numbers, booleans and nulls) as well as the begin
+ * and end delimiters of objects and arrays.
+ *
+ * Encoding JSON
+ * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON
+ * document must contain one top-level array or object. Call methods on the
+ * writer as you walk the structure's contents, nesting arrays and objects as
+ * necessary:
+ *
+ *
+ *
+ * Example
+ * Suppose we'd like to encode a stream of messages such as the following: {@code
+ * [
+ * {
+ * "id": 912345678901,
+ * "text": "How do I write JSON on Android?",
+ * "geo": null,
+ * "user": {
+ * "name": "android_newb",
+ * "followers_count": 41
+ * }
+ * },
+ * {
+ * "id": 912345678902,
+ * "text": "@android_newb just use android.util.JsonWriter!",
+ * "geo": [50.454722, -104.606667],
+ * "user": {
+ * "name": "jesse",
+ * "followers_count": 2
+ * }
+ * }
+ * ]}
+ * This code encodes the above structure: {@code
+ * public void writeJsonStream(OutputStream out, List
+ *
+ *
+ *
+ */
+ public void setLenient(boolean lenient) {
+ this.lenient = lenient;
+ }
+
+ /**
+ * Returns true if this writer has relaxed syntax rules.
+ */
+ public boolean isLenient() {
+ return lenient;
+ }
+
+ /**
+ * Begins encoding a new array. Each call to this method must be paired with
+ * a call to {@link #endArray}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter beginArray() throws IOException {
+ return open(JsonScope.EMPTY_ARRAY, "[");
+ }
+
+ /**
+ * Ends encoding the current array.
+ *
+ * @return this writer.
+ */
+ public JsonWriter endArray() throws IOException {
+ return close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, "]");
+ }
+
+ /**
+ * Begins encoding a new object. Each call to this method must be paired
+ * with a call to {@link #endObject}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter beginObject() throws IOException {
+ return open(JsonScope.EMPTY_OBJECT, "{");
+ }
+
+ /**
+ * Ends encoding the current object.
+ *
+ * @return this writer.
+ */
+ public JsonWriter endObject() throws IOException {
+ return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, "}");
+ }
+
+ /**
+ * Enters a new scope by appending any necessary whitespace and the given
+ * bracket.
+ */
+ private JsonWriter open(JsonScope empty, String openBracket) throws IOException {
+ beforeValue(true);
+ stack.add(empty);
+ out.write(openBracket);
+ return this;
+ }
+
+ /**
+ * Closes the current scope by appending any necessary whitespace and the
+ * given bracket.
+ */
+ private JsonWriter close(JsonScope empty, JsonScope nonempty, String closeBracket)
+ throws IOException {
+ JsonScope context = peek();
+ if (context != nonempty && context != empty) {
+ throw new IllegalStateException("Nesting problem: " + stack);
+ }
+
+ stack.remove(stack.size() - 1);
+ if (context == nonempty) {
+ newline();
+ }
+ out.write(closeBracket);
+ return this;
+ }
+
+ /**
+ * Returns the value on the top of the stack.
+ */
+ private JsonScope peek() {
+ return stack.get(stack.size() - 1);
+ }
+
+ /**
+ * Replace the value on the top of the stack with the given value.
+ */
+ private void replaceTop(JsonScope topOfStack) {
+ stack.set(stack.size() - 1, topOfStack);
+ }
+
+ /**
+ * Encodes the property name.
+ *
+ * @param name the name of the forthcoming value. May not be null.
+ * @return this writer.
+ */
+ public JsonWriter name(String name) throws IOException {
+ if (name == null) {
+ throw new NullPointerException("name == null");
+ }
+ beforeName();
+ string(name);
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @param value the literal string value, or null to encode a null literal.
+ * @return this writer.
+ */
+ public JsonWriter value(String value) throws IOException {
+ if (value == null) {
+ return nullValue();
+ }
+ beforeValue(false);
+ string(value);
+ return this;
+ }
+
+ /**
+ * Encodes {@code null}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter nullValue() throws IOException {
+ beforeValue(false);
+ out.write("null");
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter value(boolean value) throws IOException {
+ beforeValue(false);
+ out.write(value ? "true" : "false");
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities} unless this writer is lenient.
+ * @return this writer.
+ */
+ public JsonWriter value(double value) throws IOException {
+ if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
+ throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
+ }
+ beforeValue(false);
+ out.append(Double.toString(value));
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter value(long value) throws IOException {
+ beforeValue(false);
+ out.write(Long.toString(value));
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities} unless this writer is lenient.
+ * @return this writer.
+ */
+ public JsonWriter value(Number value) throws IOException {
+ if (value == null) {
+ return nullValue();
+ }
+
+ String string = value.toString();
+ if (!lenient &&
+ (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
+ throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
+ }
+ beforeValue(false);
+ out.append(string);
+ return this;
+ }
+
+ /**
+ * Ensures all buffered data is written to the underlying {@link Writer}
+ * and flushes that writer.
+ */
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ /**
+ * Flushes and closes this writer and the underlying {@link Writer}.
+ *
+ * @throws IOException if the JSON document is incomplete.
+ */
+ public void close() throws IOException {
+ out.close();
+
+ if (peek() != JsonScope.NONEMPTY_DOCUMENT) {
+ throw new IOException("Incomplete document");
+ }
+ }
+
+ private void string(String value) throws IOException {
+ out.write("\"");
+ for (int i = 0, length = value.length(); i < length; i++) {
+ char c = value.charAt(i);
+
+ /*
+ * From RFC 4627, "All Unicode characters may be placed within the
+ * quotation marks except for the characters that must be escaped:
+ * quotation mark, reverse solidus, and the control characters
+ * (U+0000 through U+001F)."
+ *
+ * We also escape '\u2028' and '\u2029', which JavaScript interprets
+ * as newline characters. This prevents eval() from failing with a
+ * syntax error.
+ * http://code.google.com/p/google-gson/issues/detail?id=341
+ */
+ switch (c) {
+ case '"':
+ case '\\':
+ out.write('\\');
+ out.write(c);
+ break;
+
+ case '\t':
+ out.write("\\t");
+ break;
+
+ case '\b':
+ out.write("\\b");
+ break;
+
+ case '\n':
+ out.write("\\n");
+ break;
+
+ case '\r':
+ out.write("\\r");
+ break;
+
+ case '\f':
+ out.write("\\f");
+ break;
+
+ case '\u2028':
+ case '\u2029':
+ out.write(String.format("\\u%04x", (int) c));
+ break;
+
+ default:
+ if (c <= 0x1F) {
+ out.write(String.format("\\u%04x", (int) c));
+ } else {
+ out.write(c);
+ }
+ break;
+ }
+
+ }
+ out.write("\"");
+ }
+
+ private void newline() throws IOException {
+ if (indent == null) {
+ return;
+ }
+
+ out.write("\n");
+ for (int i = 1; i < stack.size(); i++) {
+ out.write(indent);
+ }
+ }
+
+ /**
+ * Inserts any necessary separators and whitespace before a name. Also
+ * adjusts the stack to expect the name's value.
+ */
+ private void beforeName() throws IOException {
+ JsonScope context = peek();
+ if (context == JsonScope.NONEMPTY_OBJECT) { // first in object
+ out.write(',');
+ } else if (context != JsonScope.EMPTY_OBJECT) { // not in an object!
+ throw new IllegalStateException("Nesting problem: " + stack);
+ }
+ newline();
+ replaceTop(JsonScope.DANGLING_NAME);
+ }
+
+ /**
+ * Inserts any necessary separators and whitespace before a literal value,
+ * inline array, or inline object. Also adjusts the stack to expect either a
+ * closing bracket or another element.
+ *
+ * @param root true if the value is a new array or object, the two values
+ * permitted as top-level elements.
+ */
+ private void beforeValue(boolean root) throws IOException {
+ switch (peek()) {
+ case EMPTY_DOCUMENT: // first in document
+ if (!lenient && !root) {
+ throw new IllegalStateException(
+ "JSON must start with an array or an object.");
+ }
+ replaceTop(JsonScope.NONEMPTY_DOCUMENT);
+ break;
+
+ case EMPTY_ARRAY: // first in array
+ replaceTop(JsonScope.NONEMPTY_ARRAY);
+ newline();
+ break;
+
+ case NONEMPTY_ARRAY: // another in array
+ out.append(',');
+ newline();
+ break;
+
+ case DANGLING_NAME: // value for name
+ out.append(separator);
+ replaceTop(JsonScope.NONEMPTY_OBJECT);
+ break;
+
+ case NONEMPTY_DOCUMENT:
+ throw new IllegalStateException(
+ "JSON must have only one top-level value.");
+
+ default:
+ throw new IllegalStateException("Nesting problem: " + stack);
+ }
+ }
+}
diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build
index a5f517ba..23f87d60 100644
--- a/src/api-impl/meson.build
+++ b/src/api-impl/meson.build
@@ -370,6 +370,10 @@ hax_jar = jar('hax', [
'android/util/DecompiledXmlResourceParser.java',
'android/util/DisplayMetrics.java',
'android/util/FloatMath.java',
+ 'android/util/JsonReader.java',
+ 'android/util/JsonScope.java',
+ 'android/util/JsonToken.java',
+ 'android/util/JsonWriter.java',
'android/util/LayoutDirection.java',
'android/util/Log.java',
'android/util/LongSparseArray.java',