Adding JsonReader.setLenient() to handle malformed JSON strings.

Also replacing setIndentSpaces() with a more general purpose method,
setIndent().

Change-Id: I64fbe4901aec23de5392362c1d40b77bc2b5566b
This commit is contained in:
Jesse Wilson
2010-08-06 16:08:59 -07:00
parent 1ddf340bfc
commit 1ba4171405
5 changed files with 405 additions and 83 deletions

View File

@@ -180354,6 +180354,19 @@
<exception name="IOException" type="java.io.IOException">
</exception>
</method>
<method name="setLenient"
return="void"
abstract="false"
native="false"
synchronized="false"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="lenient" type="boolean">
</parameter>
</method>
<method name="skipValue"
return="void"
abstract="false"
@@ -180424,6 +180437,8 @@
deprecated="not deprecated"
visibility="public"
>
<implements name="java.io.Closeable">
</implements>
<constructor name="JsonWriter"
type="android.util.JsonWriter"
static="false"
@@ -180540,7 +180555,7 @@
<exception name="IOException" type="java.io.IOException">
</exception>
</method>
<method name="setIndentSpaces"
<method name="setIndent"
return="void"
abstract="false"
native="false"
@@ -180550,7 +180565,7 @@
deprecated="not deprecated"
visibility="public"
>
<parameter name="indent" type="int">
<parameter name="indent" type="java.lang.String">
</parameter>
</method>
<method name="value"
@@ -272628,7 +272643,7 @@
deprecated="not deprecated"
visibility="public"
>
<parameter name="loop" type="boolean">
<parameter name="disable" type="boolean">
</parameter>
<exception name="SocketException" type="java.net.SocketException">
</exception>
@@ -274187,7 +274202,7 @@
deprecated="not deprecated"
visibility="public"
>
<parameter name="value" type="boolean">
<parameter name="keepAlive" type="boolean">
</parameter>
<exception name="SocketException" type="java.net.SocketException">
</exception>

View File

@@ -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 <a
* href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the
* parser to lenient causes it to ignore the following syntax errors:
*
* <ul>
* <li>End of line comments starting with {@code //} or {@code #} and
* ending with a newline character.
* <li>C-style comments starting with {@code /*} and ending with
* {@code *}{@code /}. Such comments may not be nested.
* <li>Names that are unquoted or {@code 'single quoted'}.
* <li>Strings that are unquoted or {@code 'single quoted'}.
* <li>Array elements separated by {@code ;} instead of {@code ,}.
* <li>Unnecessary array separators. These are interpreted as if null
* was the omitted value.
* <li>Names and values separated by {@code =} or {@code =>} instead of
* {@code :}.
* <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
* </ul>
*/
public 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;
}
}

View File

@@ -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 = ": ";
}
}

View File

@@ -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();
}
}

View File

@@ -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);