diff --git a/api/current.xml b/api/current.xml
index 064b998dbf054..4b4075874fc71 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -180354,6 +180354,19 @@
+
+
+
+
+
+
-
-
+
-
+
@@ -274187,7 +274202,7 @@
deprecated="not deprecated"
visibility="public"
>
-
+
diff --git a/core/java/android/util/JsonReader.java b/core/java/android/util/JsonReader.java
index e47be0d6d28c7..a2dffce059ff5 100644
--- a/core/java/android/util/JsonReader.java
+++ b/core/java/android/util/JsonReader.java
@@ -172,6 +172,9 @@ public final class JsonReader implements Closeable {
/** 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.
@@ -207,9 +210,6 @@ public final class JsonReader implements Closeable {
/** 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}.
*/
@@ -220,6 +220,31 @@ public final class JsonReader implements Closeable {
this.in = in;
}
+ /**
+ * Configure this parser to be be liberal in what it accepts. By default,
+ * this parser is strict and only accepts JSON as specified by RFC 4627. Setting the
+ * parser to lenient causes it to ignore the following syntax errors:
+ *
+ *
+ * - End of line comments starting with {@code //} or {@code #} and
+ * ending with a newline character.
+ *
- C-style comments starting with {@code /*} and ending with
+ * {@code *}{@code /}. Such comments may not be nested.
+ *
- Names that are unquoted or {@code 'single quoted'}.
+ *
- Strings that are unquoted or {@code 'single quoted'}.
+ *
- Array elements separated by {@code ;} instead of {@code ,}.
+ *
- Unnecessary array separators. These are interpreted as if null
+ * was the omitted value.
+ *
- Names and values separated by {@code =} or {@code =>} instead of
+ * {@code :}.
+ *
- Name/value pairs separated by {@code ;} instead of {@code ,}.
+ *
+ */
+ public void setLenient(boolean lenient) {
+ this.lenient = lenient;
+ }
+
/**
* Consumes the next token from the JSON stream and asserts that it is the
* beginning of a new array.
@@ -253,11 +278,12 @@ public final class JsonReader implements Closeable {
}
/**
- * Consumes {@code token}.
+ * Consumes {@code expected}.
*/
- private void expect(JsonToken token) throws IOException {
- if (quickPeek() != token) {
- throw new IllegalStateException("Expected " + token + " but was " + peek());
+ private void expect(JsonToken expected) throws IOException {
+ quickPeek();
+ if (token != expected) {
+ throw new IllegalStateException("Expected " + expected + " but was " + peek());
}
advance();
}
@@ -266,8 +292,8 @@ public final class JsonReader implements Closeable {
* 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;
+ quickPeek();
+ return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY;
}
/**
@@ -285,11 +311,8 @@ public final class JsonReader implements Closeable {
/**
* 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.
+ * {@code value} will be non-null. To ensure {@code token} has a definitive
+ * value, use {@link #peek()}
*/
private JsonToken quickPeek() throws IOException {
if (hasToken) {
@@ -347,7 +370,8 @@ public final class JsonReader implements Closeable {
* name.
*/
public String nextName() throws IOException {
- if (quickPeek() != JsonToken.NAME) {
+ quickPeek();
+ if (token != JsonToken.NAME) {
throw new IllegalStateException("Expected a name but was " + peek());
}
String result = name;
@@ -364,8 +388,8 @@ public final class JsonReader implements Closeable {
* this reader is closed.
*/
public String nextString() throws IOException {
- JsonToken peek = peek();
- if (value == null || (peek != JsonToken.STRING && peek != JsonToken.NUMBER)) {
+ peek();
+ if (value == null || (token != JsonToken.STRING && token != JsonToken.NUMBER)) {
throw new IllegalStateException("Expected a string but was " + peek());
}
@@ -382,8 +406,8 @@ public final class JsonReader implements Closeable {
* this reader is closed.
*/
public boolean nextBoolean() throws IOException {
- JsonToken peek = quickPeek();
- if (value == null || peek == JsonToken.STRING) {
+ quickPeek();
+ if (value == null || token == JsonToken.STRING) {
throw new IllegalStateException("Expected a boolean but was " + peek());
}
@@ -408,8 +432,8 @@ public final class JsonReader implements Closeable {
* reader is closed.
*/
public void nextNull() throws IOException {
- JsonToken peek = quickPeek();
- if (value == null || peek == JsonToken.STRING) {
+ quickPeek();
+ if (value == null || token == JsonToken.STRING) {
throw new IllegalStateException("Expected null but was " + peek());
}
@@ -570,36 +594,43 @@ public final class JsonReader implements Closeable {
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--;
- }
+ replaceTop(JsonScope.NONEMPTY_ARRAY);
} else {
+ /* Look for a comma before each element after the first element. */
switch (nextNonWhitespace()) {
case ']':
pop();
hasToken = true;
return token = JsonToken.END_ARRAY;
- case ',':
case ';':
+ checkLenient(); // fall-through
+ case ',':
break;
default:
throw syntaxError("Unterminated array");
}
}
- return nextValue();
+ switch (nextNonWhitespace()) {
+ case ']':
+ if (firstElement) {
+ pop();
+ hasToken = true;
+ return token = JsonToken.END_ARRAY;
+ }
+ // fall-through to handle ",]"
+ case ';':
+ case ',':
+ /* In lenient mode, a 0-length literal means 'null' */
+ checkLenient();
+ pos--;
+ hasToken = true;
+ value = "null";
+ return token = JsonToken.NULL;
+ default:
+ pos--;
+ return nextValue();
+ }
}
private JsonToken nextInObject(boolean firstElement) throws IOException {
@@ -636,10 +667,12 @@ public final class JsonReader implements Closeable {
int quote = nextNonWhitespace();
switch (quote) {
case '\'':
+ checkLenient(); // fall-through
case '"':
name = nextString((char) quote);
break;
default:
+ checkLenient();
pos--;
name = nextLiteral();
if (name.isEmpty()) {
@@ -653,19 +686,21 @@ public final class JsonReader implements Closeable {
}
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.
+ * Read the name/value separator. Usually a colon ':'. In lenient mode
+ * we also accept an equals sign '=', or an arrow "=>".
*/
- int separator = nextNonWhitespace();
- if (separator != ':' && separator != '=') {
- throw syntaxError("Expected ':'");
- }
- if (separator == '=' && (pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
- pos++;
+ switch (nextNonWhitespace()) {
+ case ':':
+ break;
+ case '=':
+ checkLenient();
+ if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
+ pos++;
+ }
+ break;
+ default:
+ throw syntaxError("Expected ':'");
}
replaceTop(JsonScope.NONEMPTY_OBJECT);
@@ -686,6 +721,7 @@ public final class JsonReader implements Closeable {
return token = JsonToken.BEGIN_ARRAY;
case '\'':
+ checkLenient(); // fall-through
case '"':
value = nextString((char) c);
hasToken = true;
@@ -722,8 +758,6 @@ public final class JsonReader implements Closeable {
}
private int nextNonWhitespace() throws IOException {
- // TODO: no comments in strict mode
-
while (pos < limit || fillBuffer(1)) {
int c = buffer[pos++];
switch (c) {
@@ -738,6 +772,7 @@ public final class JsonReader implements Closeable {
return c;
}
+ checkLenient();
char peek = buffer[pos];
switch (peek) {
case '*':
@@ -765,6 +800,7 @@ public final class JsonReader implements Closeable {
* specify this behaviour, but it's required to parse
* existing documents. See http://b/2571423.
*/
+ checkLenient();
skipToEndOfLine();
continue;
@@ -776,6 +812,12 @@ public final class JsonReader implements Closeable {
throw syntaxError("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
@@ -853,9 +895,6 @@ public final class JsonReader implements Closeable {
* 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. */
@@ -863,17 +902,19 @@ public final class JsonReader implements Closeable {
while (pos < limit) {
int c = buffer[pos++];
switch (c) {
+ case '/':
+ case '\\':
+ case ';':
+ case '#':
+ case '=':
+ checkLenient(); // fall-through
+
case '{':
case '}':
case '[':
case ']':
- case '/':
- case '\\':
case ':':
- case '=':
case ',':
- case ';':
- case '#':
case ' ':
case '\t':
case '\f':
@@ -965,7 +1006,7 @@ public final class JsonReader implements Closeable {
/**
* Assigns {@code nextToken} based on the value of {@code nextValue}.
*/
- private void decodeLiteral() {
+ private void decodeLiteral() throws IOException {
if (value.equalsIgnoreCase("null")) {
token = JsonToken.NULL;
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
@@ -975,7 +1016,8 @@ public final class JsonReader implements Closeable {
Double.parseDouble(value); // this work could potentially be cached
token = JsonToken.NUMBER;
} catch (NumberFormatException ignored) {
- /* an unquoted string. This document is not well-formed! */
+ // this must be an unquoted string
+ checkLenient();
token = JsonToken.STRING;
}
}
diff --git a/core/java/android/util/JsonWriter.java b/core/java/android/util/JsonWriter.java
index 913b04784d98c..fecc1c8737712 100644
--- a/core/java/android/util/JsonWriter.java
+++ b/core/java/android/util/JsonWriter.java
@@ -16,10 +16,10 @@
package android.util;
+import java.io.Closeable;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
@@ -117,7 +117,7 @@ import java.util.List;
* 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 {
+public final class JsonWriter implements Closeable {
/** The output data, containing at most one top-level array or object. */
private final Writer out;
@@ -151,23 +151,20 @@ public final class JsonWriter {
}
/**
- * Sets the number of spaces to indent each line in the encoded document.
- * If {@code indent == 0} the encoded document will be compact. If {@code
- * indent > 0}, the encoded document will be more human-readable.
+ * Sets the indentation string to be repeated for each level of indentation
+ * in the encoded document. If {@code indent.isEmpty()} the encoded document
+ * will be compact. Otherwise the encoded document will be more
+ * human-readable.
+ *
+ * @param indent a string containing only whitespace.
*/
- public void setIndentSpaces(int indent) {
- if (indent < 0) {
- throw new IllegalArgumentException("indent < 0");
- }
-
- if (indent > 0) {
- char[] indentChars = new char[indent];
- Arrays.fill(indentChars, ' ');
- this.indent = new String(indentChars);
- this.separator = ": ";
- } else {
+ public void setIndent(String indent) {
+ if (indent.isEmpty()) {
this.indent = null;
this.separator = ":";
+ } else {
+ this.indent = indent;
+ this.separator = ": ";
}
}
diff --git a/core/tests/coretests/src/android/util/JsonReaderTest.java b/core/tests/coretests/src/android/util/JsonReaderTest.java
index b0cfb7bf4364b..ced9310d5d5db 100644
--- a/core/tests/coretests/src/android/util/JsonReaderTest.java
+++ b/core/tests/coretests/src/android/util/JsonReaderTest.java
@@ -32,6 +32,14 @@ public final class JsonReaderTest extends TestCase {
assertEquals(JsonToken.END_DOCUMENT, reader.peek());
}
+ public void testReadEmptyArray() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[]"));
+ reader.beginArray();
+ assertFalse(reader.hasNext());
+ reader.endArray();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+ }
+
public void testReadObject() throws IOException {
JsonReader reader = new JsonReader(new StringReader(
"{\"a\": \"android\", \"b\": \"banana\"}"));
@@ -44,6 +52,14 @@ public final class JsonReaderTest extends TestCase {
assertEquals(JsonToken.END_DOCUMENT, reader.peek());
}
+ public void testReadEmptyObject() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{}"));
+ reader.beginObject();
+ assertFalse(reader.hasNext());
+ reader.endObject();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+ }
+
public void testSkipObject() throws IOException {
JsonReader reader = new JsonReader(new StringReader(
"{\"a\": { \"c\": [], \"d\": [true, true, {}] }, \"b\": \"banana\"}"));
@@ -412,4 +428,256 @@ public final class JsonReaderTest extends TestCase {
} catch (IllegalStateException expected) {
}
}
+
+ public void testStrictNameValueSeparator() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{\"a\"=true}"));
+ reader.beginObject();
+ assertEquals("a", reader.nextName());
+ try {
+ reader.nextBoolean();
+ fail();
+ } catch (IOException expected) {
+ }
+
+ reader = new JsonReader(new StringReader("{\"a\"=>true}"));
+ reader.beginObject();
+ assertEquals("a", reader.nextName());
+ try {
+ reader.nextBoolean();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientNameValueSeparator() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{\"a\"=true}"));
+ reader.setLenient(true);
+ reader.beginObject();
+ assertEquals("a", reader.nextName());
+ assertEquals(true, reader.nextBoolean());
+
+ reader = new JsonReader(new StringReader("{\"a\"=>true}"));
+ reader.setLenient(true);
+ reader.beginObject();
+ assertEquals("a", reader.nextName());
+ assertEquals(true, reader.nextBoolean());
+ }
+
+ public void testStrictComments() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[// comment \n true]"));
+ reader.beginArray();
+ try {
+ reader.nextBoolean();
+ fail();
+ } catch (IOException expected) {
+ }
+
+ reader = new JsonReader(new StringReader("[# comment \n true]"));
+ reader.beginArray();
+ try {
+ reader.nextBoolean();
+ fail();
+ } catch (IOException expected) {
+ }
+
+ reader = new JsonReader(new StringReader("[/* comment */ true]"));
+ reader.beginArray();
+ try {
+ reader.nextBoolean();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientComments() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[// comment \n true]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ assertEquals(true, reader.nextBoolean());
+
+ reader = new JsonReader(new StringReader("[# comment \n true]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ assertEquals(true, reader.nextBoolean());
+
+ reader = new JsonReader(new StringReader("[/* comment */ true]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ assertEquals(true, reader.nextBoolean());
+ }
+
+ public void testStrictUnquotedNames() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{a:true}"));
+ reader.beginObject();
+ try {
+ reader.nextName();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientUnquotedNames() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{a:true}"));
+ reader.setLenient(true);
+ reader.beginObject();
+ assertEquals("a", reader.nextName());
+ }
+
+ public void testStrictSingleQuotedNames() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{'a':true}"));
+ reader.beginObject();
+ try {
+ reader.nextName();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientSingleQuotedNames() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{'a':true}"));
+ reader.setLenient(true);
+ reader.beginObject();
+ assertEquals("a", reader.nextName());
+ }
+
+ public void testStrictUnquotedStrings() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[a]"));
+ reader.beginArray();
+ try {
+ reader.nextString();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientUnquotedStrings() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[a]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ assertEquals("a", reader.nextString());
+ }
+
+ public void testStrictSingleQuotedStrings() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("['a']"));
+ reader.beginArray();
+ try {
+ reader.nextString();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientSingleQuotedStrings() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("['a']"));
+ reader.setLenient(true);
+ reader.beginArray();
+ assertEquals("a", reader.nextString());
+ }
+
+ public void testStrictSemicolonDelimitedArray() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[true;true]"));
+ reader.beginArray();
+ try {
+ reader.nextBoolean();
+ reader.nextBoolean();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientSemicolonDelimitedArray() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[true;true]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ assertEquals(true, reader.nextBoolean());
+ assertEquals(true, reader.nextBoolean());
+ }
+
+ public void testStrictSemicolonDelimitedNameValuePair() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{\"a\":true;\"b\":true}"));
+ reader.beginObject();
+ assertEquals("a", reader.nextName());
+ try {
+ reader.nextBoolean();
+ reader.nextName();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientSemicolonDelimitedNameValuePair() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("{\"a\":true;\"b\":true}"));
+ reader.setLenient(true);
+ reader.beginObject();
+ assertEquals("a", reader.nextName());
+ assertEquals(true, reader.nextBoolean());
+ assertEquals("b", reader.nextName());
+ }
+
+ public void testStrictUnnecessaryArraySeparators() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[true,,true]"));
+ reader.beginArray();
+ assertEquals(true, reader.nextBoolean());
+ try {
+ reader.nextNull();
+ fail();
+ } catch (IOException expected) {
+ }
+
+ reader = new JsonReader(new StringReader("[,true]"));
+ reader.beginArray();
+ try {
+ reader.nextNull();
+ fail();
+ } catch (IOException expected) {
+ }
+
+ reader = new JsonReader(new StringReader("[true,]"));
+ reader.beginArray();
+ assertEquals(true, reader.nextBoolean());
+ try {
+ reader.nextNull();
+ fail();
+ } catch (IOException expected) {
+ }
+
+ reader = new JsonReader(new StringReader("[,]"));
+ reader.beginArray();
+ try {
+ reader.nextNull();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testLenientUnnecessaryArraySeparators() throws IOException {
+ JsonReader reader = new JsonReader(new StringReader("[true,,true]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ assertEquals(true, reader.nextBoolean());
+ reader.nextNull();
+ assertEquals(true, reader.nextBoolean());
+ reader.endArray();
+
+ reader = new JsonReader(new StringReader("[,true]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ reader.nextNull();
+ assertEquals(true, reader.nextBoolean());
+ reader.endArray();
+
+ reader = new JsonReader(new StringReader("[true,]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ assertEquals(true, reader.nextBoolean());
+ reader.nextNull();
+ reader.endArray();
+
+ reader = new JsonReader(new StringReader("[,]"));
+ reader.setLenient(true);
+ reader.beginArray();
+ reader.nextNull();
+ reader.nextNull();
+ reader.endArray();
+ }
}
diff --git a/core/tests/coretests/src/android/util/JsonWriterTest.java b/core/tests/coretests/src/android/util/JsonWriterTest.java
index 0bf7e042a7ac9..fa840239d7b80 100644
--- a/core/tests/coretests/src/android/util/JsonWriterTest.java
+++ b/core/tests/coretests/src/android/util/JsonWriterTest.java
@@ -346,7 +346,7 @@ public final class JsonWriterTest extends TestCase {
public void testPrettyPrintObject() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
- jsonWriter.setIndentSpaces(3);
+ jsonWriter.setIndent(" ");
jsonWriter.beginObject();
jsonWriter.name("a").value(true);
@@ -383,7 +383,7 @@ public final class JsonWriterTest extends TestCase {
public void testPrettyPrintArray() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
- jsonWriter.setIndentSpaces(3);
+ jsonWriter.setIndent(" ");
jsonWriter.beginArray();
jsonWriter.value(true);