Merge pull request #2 from square/jwilson_0809_momoshi

Make JsonReader and JsonWriter our own.
This commit is contained in:
Jesse Wilson 2014-08-10 00:13:04 -04:00
commit 12243bc6fc
5 changed files with 575 additions and 807 deletions

View file

@ -18,7 +18,9 @@ package com.squareup.moshi;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.Reader;
import okio.Buffer;
import okio.BufferedSource;
import okio.Source;
/**
* Reads a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
@ -166,26 +168,12 @@ import java.io.Reader;
* precision loss, extremely large values should be written and read as strings
* in JSON.
*
* <a name="nonexecuteprefix"/><h3>Non-Execute Prefix</h3>
* Web servers that serve private data using JSON may be vulnerable to <a
* href="http://en.wikipedia.org/wiki/JSON#Cross-site_request_forgery">Cross-site
* request forgery</a> attacks. In such an attack, a malicious site gains access
* to a private JSON file by executing it with an HTML {@code <script>} tag.
*
* <p>Prefixing JSON files with <code>")]}'\n"</code> makes them non-executable
* by {@code <script>} tags, disarming the attack. Since the prefix is malformed
* JSON, strict parsing fails when it is encountered. This class permits the
* non-execute prefix when {@link #setLenient(boolean) lenient parsing} is
* enabled.
*
* <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances
* of this class are not thread safe.
*
* @author Jesse Wilson
*/
public class JsonReader implements Closeable {
/** The only non-execute prefix this parser permits */
private static final char[] NON_EXECUTE_PREFIX = ")]}'\n".toCharArray();
private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;
private static final int PEEKED_NONE = 0;
@ -219,24 +207,13 @@ public class JsonReader implements Closeable {
private static final int NUMBER_CHAR_EXP_SIGN = 6;
private static final int NUMBER_CHAR_EXP_DIGIT = 7;
/** 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;
private int lineNumber = 0;
private int lineStart = 0;
/** The input JSON. */
private final BufferedSource source;
private final Buffer buffer;
private int peeked = PEEKED_NONE;
@ -282,11 +259,19 @@ public class JsonReader implements Closeable {
/**
* 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");
public JsonReader(Source source) {
if (source == null) {
throw new NullPointerException("source == null");
}
this.in = in;
throw new UnsupportedOperationException("TODO");
}
/**
* Creates a new instance that reads a JSON-encoded value {@code s}.
*/
public JsonReader(String s) {
this.source = new Buffer().writeUtf8(s);
this.buffer = new Buffer();
}
/**
@ -344,7 +329,7 @@ public class JsonReader implements Closeable {
peeked = PEEKED_NONE;
} else {
throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
}
@ -362,7 +347,7 @@ public class JsonReader implements Closeable {
peeked = PEEKED_NONE;
} else {
throw new IllegalStateException("Expected END_ARRAY but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
}
@ -380,7 +365,7 @@ public class JsonReader implements Closeable {
peeked = PEEKED_NONE;
} else {
throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
}
@ -399,7 +384,7 @@ public class JsonReader implements Closeable {
peeked = PEEKED_NONE;
} else {
throw new IllegalStateException("Expected END_OBJECT but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
}
@ -463,6 +448,7 @@ public class JsonReader implements Closeable {
} else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
// Look for a comma before the next element.
int c = nextNonWhitespace(true);
buffer.readByte(); // consume ']' or ','.
switch (c) {
case ']':
return peeked = PEEKED_END_ARRAY;
@ -478,6 +464,7 @@ public class JsonReader implements Closeable {
// Look for a comma before the next element.
if (peekStack == JsonScope.NONEMPTY_OBJECT) {
int c = nextNonWhitespace(true);
buffer.readByte(); // Consume '}' or ','.
switch (c) {
case '}':
return peeked = PEEKED_END_OBJECT;
@ -492,19 +479,21 @@ public class JsonReader implements Closeable {
int c = nextNonWhitespace(true);
switch (c) {
case '"':
buffer.readByte(); // consume the '\"'.
return peeked = PEEKED_DOUBLE_QUOTED_NAME;
case '\'':
buffer.readByte(); // consume the '\''.
checkLenient();
return peeked = PEEKED_SINGLE_QUOTED_NAME;
case '}':
if (peekStack != JsonScope.NONEMPTY_OBJECT) {
buffer.readByte(); // consume the '}'.
return peeked = PEEKED_END_OBJECT;
} else {
throw syntaxError("Expected name");
}
default:
checkLenient();
pos--; // Don't consume the first character in an unquoted string.
if (isLiteral((char) c)) {
return peeked = PEEKED_UNQUOTED_NAME;
} else {
@ -515,22 +504,20 @@ public class JsonReader implements Closeable {
stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;
// Look for a colon before the value.
int c = nextNonWhitespace(true);
buffer.readByte(); // Consume ':'.
switch (c) {
case ':':
break;
case '=':
checkLenient();
if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
pos++;
if (fillBuffer(1) && buffer.getByte(0) == '>') {
buffer.readByte(); // Consume '>'.
}
break;
default:
throw syntaxError("Expected ':'");
}
} else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
if (lenient) {
consumeNonExecutePrefix();
}
stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
} else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
int c = nextNonWhitespace(false);
@ -538,7 +525,6 @@ public class JsonReader implements Closeable {
return peeked = PEEKED_EOF;
} else {
checkLenient();
pos--;
}
} else if (peekStack == JsonScope.CLOSED) {
throw new IllegalStateException("JsonReader is closed");
@ -548,6 +534,7 @@ public class JsonReader implements Closeable {
switch (c) {
case ']':
if (peekStack == JsonScope.EMPTY_ARRAY) {
buffer.readByte(); // Consume ']'.
return peeked = PEEKED_END_ARRAY;
}
// fall-through to handle ",]"
@ -556,25 +543,27 @@ public class JsonReader implements Closeable {
// In lenient mode, a 0-length literal in an array means 'null'.
if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) {
checkLenient();
pos--;
return peeked = PEEKED_NULL;
} else {
throw syntaxError("Unexpected value");
}
case '\'':
checkLenient();
buffer.readByte(); // Consume '\''.
return peeked = PEEKED_SINGLE_QUOTED;
case '"':
if (stackSize == 1) {
checkLenient();
}
buffer.readByte(); // Consume '\"'.
return peeked = PEEKED_DOUBLE_QUOTED;
case '[':
buffer.readByte(); // Consume '['.
return peeked = PEEKED_BEGIN_ARRAY;
case '{':
buffer.readByte(); // Consume ']'.
return peeked = PEEKED_BEGIN_OBJECT;
default:
pos--; // Don't consume the first character in a literal value.
}
if (stackSize == 1) {
@ -591,7 +580,7 @@ public class JsonReader implements Closeable {
return result;
}
if (!isLiteral(buffer[pos])) {
if (!isLiteral(buffer.getByte(0))) {
throw syntaxError("Expected value");
}
@ -601,7 +590,7 @@ public class JsonReader implements Closeable {
private int peekKeyword() throws IOException {
// Figure out which keyword we're matching against by its first character.
char c = buffer[pos];
byte c = buffer.getByte(0);
String keyword;
String keywordUpper;
int peeking;
@ -624,31 +613,25 @@ public class JsonReader implements Closeable {
// Confirm that chars [1..length) match the keyword.
int length = keyword.length();
for (int i = 1; i < length; i++) {
if (pos + i >= limit && !fillBuffer(i + 1)) {
if (!fillBuffer(i + 1)) {
return PEEKED_NONE;
}
c = buffer[pos + i];
c = buffer.getByte(i);
if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) {
return PEEKED_NONE;
}
}
if ((pos + length < limit || fillBuffer(length + 1))
&& isLiteral(buffer[pos + length])) {
if (fillBuffer(length + 1) && isLiteral(buffer.getByte(length))) {
return PEEKED_NONE; // Don't match trues, falsey or nullsoft!
}
// We've found the keyword followed either by EOF or by a non-literal character.
pos += length;
buffer.skip(length);
return peeked = peeking;
}
private int peekNumber() throws IOException {
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
char[] buffer = this.buffer;
int p = pos;
int l = limit;
long value = 0; // Negative to accommodate Long.MIN_VALUE more easily.
boolean negative = false;
boolean fitsInLong = true;
@ -658,20 +641,11 @@ public class JsonReader implements Closeable {
charactersOfNumber:
for (; true; i++) {
if (p + i == l) {
if (i == buffer.length) {
// Though this looks like a well-formed number, it's too long to continue reading. Give up
// and let the application handle this as an unquoted literal.
return PEEKED_NONE;
}
if (!fillBuffer(i + 1)) {
break;
}
p = pos;
l = limit;
if (!fillBuffer(i + 1)) {
break;
}
char c = buffer[p + i];
byte c = buffer.getByte(i);
switch (c) {
case '-':
if (last == NUMBER_CHAR_NONE) {
@ -735,7 +709,7 @@ public class JsonReader implements Closeable {
// We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)) {
peekedLong = negative ? value : -value;
pos += i;
buffer.skip(i);
return peeked = PEEKED_LONG;
} else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT
|| last == NUMBER_CHAR_EXP_DIGIT) {
@ -746,7 +720,7 @@ public class JsonReader implements Closeable {
}
}
private boolean isLiteral(char c) throws IOException {
private boolean isLiteral(int c) throws IOException {
switch (c) {
case '/':
case '\\':
@ -792,7 +766,7 @@ public class JsonReader implements Closeable {
result = nextQuotedValue('"');
} else {
throw new IllegalStateException("Expected a name but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peeked = PEEKED_NONE;
pathNames[stackSize - 1] = result;
@ -825,11 +799,10 @@ public class JsonReader implements Closeable {
} else if (p == PEEKED_LONG) {
result = Long.toString(peekedLong);
} else if (p == PEEKED_NUMBER) {
result = new String(buffer, pos, peekedNumberLength);
pos += peekedNumberLength;
result = buffer.readUtf8(peekedNumberLength);
} else {
throw new IllegalStateException("Expected a string but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
@ -858,7 +831,7 @@ public class JsonReader implements Closeable {
return false;
}
throw new IllegalStateException("Expected a boolean but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
/**
@ -878,7 +851,7 @@ public class JsonReader implements Closeable {
pathIndices[stackSize - 1]++;
} else {
throw new IllegalStateException("Expected null but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
}
@ -904,22 +877,21 @@ public class JsonReader implements Closeable {
}
if (p == PEEKED_NUMBER) {
peekedString = new String(buffer, pos, peekedNumberLength);
pos += peekedNumberLength;
peekedString = buffer.readUtf8(peekedNumberLength);
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
} else if (p == PEEKED_UNQUOTED) {
peekedString = nextUnquotedValue();
} else if (p != PEEKED_BUFFERED) {
throw new IllegalStateException("Expected a double but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peeked = PEEKED_BUFFERED;
double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
throw new IOException("JSON forbids NaN and infinities: " + result
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peekedString = null;
peeked = PEEKED_NONE;
@ -950,8 +922,7 @@ public class JsonReader implements Closeable {
}
if (p == PEEKED_NUMBER) {
peekedString = new String(buffer, pos, peekedNumberLength);
pos += peekedNumberLength;
peekedString = buffer.readUtf8(peekedNumberLength);
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
try {
@ -964,7 +935,7 @@ public class JsonReader implements Closeable {
}
} else {
throw new IllegalStateException("Expected a long but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peeked = PEEKED_BUFFERED;
@ -972,7 +943,7 @@ public class JsonReader implements Closeable {
long result = (long) asDouble;
if (result != asDouble) { // Make sure no precision was lost casting to 'long'.
throw new NumberFormatException("Expected a long but was " + peekedString
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peekedString = null;
peeked = PEEKED_NONE;
@ -991,40 +962,24 @@ public class JsonReader implements Closeable {
* malformed.
*/
private String nextQuotedValue(char quote) throws IOException {
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
char[] buffer = this.buffer;
StringBuilder builder = new StringBuilder();
while (true) {
int p = pos;
int l = limit;
/* the index of the first character not yet appended to the builder. */
int start = p;
while (p < l) {
int c = buffer[p++];
int p = 0;
while (fillBuffer(p + 1)) {
int c = buffer.getByte(p++);
if (c == quote) {
pos = p;
builder.append(buffer, start, p - start - 1);
return builder.toString();
} else if (c == '\\') {
pos = p;
builder.append(buffer, start, p - start - 1);
builder.append(readEscapeCharacter());
p = pos;
l = limit;
start = p;
} else if (c == '\n') {
lineNumber++;
lineStart = p;
}
}
builder.append(buffer, start, p - start);
pos = p;
if (!fillBuffer(1)) {
throw syntaxError("Unterminated string");
if (c == quote) {
builder.append(buffer.readUtf8(p - 1));
buffer.readByte();
return builder.toString();
} else if (c == '\\') {
builder.append(buffer.readUtf8(p - 1));
buffer.readByte(); // '\'
builder.append(readEscapeCharacter());
p = 0;
}
}
throw syntaxError("Unterminated string");
}
/**
@ -1032,121 +987,79 @@ public class JsonReader implements Closeable {
*/
@SuppressWarnings("fallthrough")
private String nextUnquotedValue() throws IOException {
StringBuilder builder = null;
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 (i < buffer.length) {
if (fillBuffer(i + 1)) {
continue;
} else {
break;
}
}
// use a StringBuilder when the value is too long. This is too long to be a number!
if (builder == null) {
builder = new StringBuilder();
}
builder.append(buffer, pos, i);
pos += i;
i = 0;
if (!fillBuffer(1)) {
break;
findNonStringChar:
for (; fillBuffer(i + 1); i++) {
switch (buffer.getByte(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 findNonStringChar;
}
}
String result;
if (builder == null) {
result = new String(buffer, pos, i);
} else {
builder.append(buffer, pos, i);
result = builder.toString();
}
pos += i;
return result;
return buffer.readUtf8(i);
}
private void skipQuotedValue(char quote) throws IOException {
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
char[] buffer = this.buffer;
do {
int p = pos;
int l = limit;
/* the index of the first character not yet appended to the builder. */
while (p < l) {
int c = buffer[p++];
if (c == quote) {
pos = p;
return;
} else if (c == '\\') {
pos = p;
readEscapeCharacter();
p = pos;
l = limit;
} else if (c == '\n') {
lineNumber++;
lineStart = p;
}
int p = 0;
while (fillBuffer(p + 1)) {
int c = buffer.getByte(p++);
if (c == quote) {
buffer.skip(p);
return;
} else if (c == '\\') {
buffer.skip(p);
readEscapeCharacter();
p = 0;
}
pos = p;
} while (fillBuffer(1));
}
throw syntaxError("Unterminated string");
}
private void skipUnquotedValue() throws IOException {
do {
int i = 0;
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':
pos += i;
return;
}
int i = 0;
findNonStringChar:
for (; fillBuffer(i + 1); i++) {
switch (buffer.getByte(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 findNonStringChar;
}
pos += i;
} while (fillBuffer(1));
}
buffer.skip(i);
}
/**
@ -1170,7 +1083,7 @@ public class JsonReader implements Closeable {
result = (int) peekedLong;
if (peekedLong != result) { // Make sure no precision was lost casting to 'int'.
throw new NumberFormatException("Expected an int but was " + peekedLong
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
@ -1178,8 +1091,7 @@ public class JsonReader implements Closeable {
}
if (p == PEEKED_NUMBER) {
peekedString = new String(buffer, pos, peekedNumberLength);
pos += peekedNumberLength;
peekedString = buffer.readUtf8(peekedNumberLength);
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
try {
@ -1192,7 +1104,7 @@ public class JsonReader implements Closeable {
}
} else {
throw new IllegalStateException("Expected an int but was " + peek()
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peeked = PEEKED_BUFFERED;
@ -1200,7 +1112,7 @@ public class JsonReader implements Closeable {
result = (int) asDouble;
if (result != asDouble) { // Make sure no precision was lost casting to 'int'.
throw new NumberFormatException("Expected an int but was " + peekedString
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
+ " at path " + getPath());
}
peekedString = null;
peeked = PEEKED_NONE;
@ -1215,7 +1127,8 @@ public class JsonReader implements Closeable {
peeked = PEEKED_NONE;
stack[0] = JsonScope.CLOSED;
stackSize = 1;
in.close();
buffer.clear();
source.close();
}
/**
@ -1250,7 +1163,7 @@ public class JsonReader implements Closeable {
} else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
skipQuotedValue('"');
} else if (p == PEEKED_NUMBER) {
pos += peekedNumberLength;
buffer.skip(peekedNumberLength);
}
peeked = PEEKED_NONE;
} while (count != 0);
@ -1280,40 +1193,10 @@ public class JsonReader implements Closeable {
* false.
*/
private boolean fillBuffer(int minimum) throws IOException {
char[] buffer = this.buffer;
lineStart -= pos;
if (limit != pos) {
limit -= pos;
System.arraycopy(buffer, pos, buffer, 0, limit);
} else {
limit = 0;
while (buffer.size() < minimum) {
if (source.read(buffer, 2048) == -1) return false;
}
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 (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') {
pos++;
lineStart++;
minimum++;
}
if (limit >= minimum) {
return true;
}
}
return false;
}
private int getLineNumber() {
return lineNumber + 1;
}
private int getColumnNumber() {
return pos - lineStart + 1;
return true;
}
/**
@ -1331,65 +1214,46 @@ public class JsonReader implements Closeable {
* before any (potentially indirect) call to fillBuffer() and reread both
* 'p' and 'l' after any (potentially indirect) call to the same method.
*/
char[] buffer = this.buffer;
int p = pos;
int l = limit;
while (true) {
if (p == l) {
pos = p;
if (!fillBuffer(1)) {
break;
}
p = pos;
l = limit;
}
int c = buffer[p++];
if (c == '\n') {
lineNumber++;
lineStart = p;
continue;
} else if (c == ' ' || c == '\r' || c == '\t') {
int p = 0;
while (fillBuffer(p + 1)) {
int c = buffer.getByte(p++);
if (c == '\n' || c == ' ' || c == '\r' || c == '\t') {
continue;
}
buffer.skip(p - 1);
if (c == '/') {
pos = p;
if (p == l) {
pos--; // push back '/' so it's still in the buffer when this method returns
boolean charsLoaded = fillBuffer(2);
pos++; // consume the '/' again
if (!charsLoaded) {
return c;
}
if (!fillBuffer(2)) {
return c;
}
checkLenient();
char peek = buffer[pos];
byte peek = buffer.getByte(1);
switch (peek) {
case '*':
// skip a /* c-style comment */
pos++;
buffer.readByte(); // '/'
buffer.readByte(); // '*'
if (!skipTo("*/")) {
throw syntaxError("Unterminated comment");
}
p = pos + 2;
l = limit;
buffer.readByte(); // '*'
buffer.readByte(); // '/'
p = 0;
continue;
case '/':
// skip a // end-of-line comment
pos++;
buffer.readByte(); // '/'
buffer.readByte(); // '/'
skipToEndOfLine();
p = pos;
l = limit;
p = 0;
continue;
default:
return c;
}
} else if (c == '#') {
pos = p;
/*
* Skip a # hash end-of-line comment. The JSON RFC doesn't
* specify this behaviour, but it's required to parse
@ -1397,16 +1261,13 @@ public class JsonReader implements Closeable {
*/
checkLenient();
skipToEndOfLine();
p = pos;
l = limit;
p = 0;
} else {
pos = p;
return c;
}
}
if (throwOnEof) {
throw new EOFException("End of input"
+ " at line " + getLineNumber() + " column " + getColumnNumber());
throw new EOFException("End of input");
} else {
return -1;
}
@ -1424,13 +1285,9 @@ public class JsonReader implements Closeable {
* caller.
*/
private void skipToEndOfLine() throws IOException {
while (pos < limit || fillBuffer(1)) {
char c = buffer[pos++];
if (c == '\n') {
lineNumber++;
lineStart = pos;
break;
} else if (c == '\r') {
while (fillBuffer(1)) {
byte c = buffer.readByte();
if (c == '\n' || c == '\r') {
break;
}
}
@ -1441,14 +1298,10 @@ public class JsonReader implements Closeable {
*/
private boolean skipTo(String toFind) throws IOException {
outer:
for (; pos + toFind.length() <= limit || fillBuffer(toFind.length()); pos++) {
if (buffer[pos] == '\n') {
lineNumber++;
lineStart = pos + 1;
continue;
}
for (; fillBuffer(toFind.length());) {
for (int c = 0; c < toFind.length(); c++) {
if (buffer[pos + c] != toFind.charAt(c)) {
if (buffer.getByte(c) != toFind.charAt(c)) {
buffer.readByte();
continue outer;
}
}
@ -1458,8 +1311,7 @@ public class JsonReader implements Closeable {
}
@Override public String toString() {
return getClass().getSimpleName()
+ " at line " + getLineNumber() + " column " + getColumnNumber();
return getClass().getSimpleName();
}
/**
@ -1503,20 +1355,20 @@ public class JsonReader implements Closeable {
* malformed.
*/
private char readEscapeCharacter() throws IOException {
if (pos == limit && !fillBuffer(1)) {
if (!fillBuffer(1)) {
throw syntaxError("Unterminated escape sequence");
}
char escaped = buffer[pos++];
byte escaped = buffer.readByte();
switch (escaped) {
case 'u':
if (pos + 4 > limit && !fillBuffer(4)) {
if (!fillBuffer(4)) {
throw syntaxError("Unterminated escape sequence");
}
// Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16);
char result = 0;
for (int i = pos, end = i + 4; i < end; i++) {
char c = buffer[i];
for (int i = 0, end = i + 4; i < end; i++) {
byte c = buffer.getByte(i);
result <<= 4;
if (c >= '0' && c <= '9') {
result += (c - '0');
@ -1525,10 +1377,10 @@ public class JsonReader implements Closeable {
} else if (c >= 'A' && c <= 'F') {
result += (c - 'A' + 10);
} else {
throw new NumberFormatException("\\u" + new String(buffer, pos, 4));
throw new NumberFormatException("\\u" + buffer.readUtf8(4));
}
}
pos += 4;
buffer.skip(4);
return result;
case 't':
@ -1547,15 +1399,11 @@ public class JsonReader implements Closeable {
return '\f';
case '\n':
lineNumber++;
lineStart = pos;
// fall-through
case '\'':
case '"':
case '\\':
default:
return escaped;
return (char) escaped;
}
}
@ -1564,29 +1412,6 @@ public class JsonReader implements Closeable {
* with this reader's content.
*/
private IOException syntaxError(String message) throws IOException {
throw new IOException(message
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
}
/**
* Consumes the non-execute prefix if it exists.
*/
private void consumeNonExecutePrefix() throws IOException {
// fast forward through the leading whitespace
nextNonWhitespace(true);
pos--;
if (pos + NON_EXECUTE_PREFIX.length > limit && !fillBuffer(NON_EXECUTE_PREFIX.length)) {
return;
}
for (int i = 0; i < NON_EXECUTE_PREFIX.length; i++) {
if (buffer[pos + i] != NON_EXECUTE_PREFIX[i]) {
return; // not a security token!
}
}
// we consumed a security token!
pos += NON_EXECUTE_PREFIX.length;
throw new IOException(message + " at path " + getPath());
}
}

View file

@ -138,7 +138,6 @@ public class JsonWriter implements Closeable, Flushable {
* error. http://code.google.com/p/google-gson/issues/detail?id=341
*/
private static final String[] REPLACEMENT_CHARS;
private static final String[] HTML_SAFE_REPLACEMENT_CHARS;
static {
REPLACEMENT_CHARS = new String[128];
for (int i = 0; i <= 0x1f; i++) {
@ -151,12 +150,6 @@ public class JsonWriter implements Closeable, Flushable {
REPLACEMENT_CHARS['\n'] = "\\n";
REPLACEMENT_CHARS['\r'] = "\\r";
REPLACEMENT_CHARS['\f'] = "\\f";
HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone();
HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c";
HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e";
HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026";
HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d";
HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
}
/** The output data, containing at most one top-level array or object. */
@ -181,11 +174,9 @@ public class JsonWriter implements Closeable, Flushable {
private boolean lenient;
private boolean htmlSafe;
private String deferredName;
private boolean serializeNulls = true;
private boolean serializeNulls;
/**
* Creates a new instance that writes a JSON-encoded stream to {@code out}.
@ -240,25 +231,6 @@ public class JsonWriter implements Closeable, Flushable {
return lenient;
}
/**
* Configure this writer to emit JSON that's safe for direct inclusion in HTML
* and XML documents. This escapes the HTML characters {@code <}, {@code >},
* {@code &} and {@code =} before writing them to the stream. Without this
* setting, your XML/HTML encoder should replace these characters with the
* corresponding escape sequences.
*/
public final void setHtmlSafe(boolean htmlSafe) {
this.htmlSafe = htmlSafe;
}
/**
* Returns true if this writer writes JSON that's safe for inclusion in HTML
* and XML documents.
*/
public final boolean isHtmlSafe() {
return htmlSafe;
}
/**
* Sets whether object members are serialized when their value is null.
* This has no impact on array elements. The default is true.
@ -528,7 +500,7 @@ public class JsonWriter implements Closeable, Flushable {
}
private void string(String value) throws IOException {
String[] replacements = htmlSafe ? HTML_SAFE_REPLACEMENT_CHARS : REPLACEMENT_CHARS;
String[] replacements = REPLACEMENT_CHARS;
out.write("\"");
int last = 0;
int length = value.length();

View file

@ -16,13 +16,13 @@
package com.squareup.moshi;
import java.io.IOException;
import java.io.StringReader;
import junit.framework.TestCase;
import org.junit.Test;
public class JsonReaderPathTest extends TestCase {
public void testPath() throws IOException {
JsonReader reader = new JsonReader(
new StringReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}"));
import static org.junit.Assert.assertEquals;
public class JsonReaderPathTest {
@Test public void path() throws IOException {
JsonReader reader = new JsonReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
assertEquals("$", reader.getPath());
reader.beginObject();
assertEquals("$.", reader.getPath());
@ -60,8 +60,8 @@ public class JsonReaderPathTest extends TestCase {
assertEquals("$", reader.getPath());
}
public void testObjectPath() throws IOException {
JsonReader reader = new JsonReader(new StringReader("{\"a\":1,\"b\":2}"));
@Test public void objectPath() throws IOException {
JsonReader reader = new JsonReader("{\"a\":1,\"b\":2}");
assertEquals("$", reader.getPath());
reader.peek();
@ -100,8 +100,8 @@ public class JsonReaderPathTest extends TestCase {
assertEquals("$", reader.getPath());
}
public void testArrayPath() throws IOException {
JsonReader reader = new JsonReader(new StringReader("[1,2]"));
@Test public void arrayPath() throws IOException {
JsonReader reader = new JsonReader("[1,2]");
assertEquals("$", reader.getPath());
reader.peek();
@ -130,8 +130,8 @@ public class JsonReaderPathTest extends TestCase {
assertEquals("$", reader.getPath());
}
public void testMultipleTopLevelValuesInOneDocument() throws IOException {
JsonReader reader = new JsonReader(new StringReader("[][]"));
@Test public void multipleTopLevelValuesInOneDocument() throws IOException {
JsonReader reader = new JsonReader("[][]");
reader.setLenient(true);
reader.beginArray();
reader.endArray();
@ -141,23 +141,23 @@ public class JsonReaderPathTest extends TestCase {
assertEquals("$", reader.getPath());
}
public void testSkipArrayElements() throws IOException {
JsonReader reader = new JsonReader(new StringReader("[1,2,3]"));
@Test public void skipArrayElements() throws IOException {
JsonReader reader = new JsonReader("[1,2,3]");
reader.beginArray();
reader.skipValue();
reader.skipValue();
assertEquals("$[2]", reader.getPath());
}
public void testSkipObjectNames() throws IOException {
JsonReader reader = new JsonReader(new StringReader("{\"a\":1}"));
@Test public void skipObjectNames() throws IOException {
JsonReader reader = new JsonReader("{\"a\":1}");
reader.beginObject();
reader.skipValue();
assertEquals("$.null", reader.getPath());
}
public void testSkipObjectValues() throws IOException {
JsonReader reader = new JsonReader(new StringReader("{\"a\":1,\"b\":2}"));
@Test public void skipObjectValues() throws IOException {
JsonReader reader = new JsonReader("{\"a\":1,\"b\":2}");
reader.beginObject();
reader.nextName();
reader.skipValue();
@ -166,8 +166,8 @@ public class JsonReaderPathTest extends TestCase {
assertEquals("$.b", reader.getPath());
}
public void testSkipNestedStructures() throws IOException {
JsonReader reader = new JsonReader(new StringReader("[[1,2,3],4]"));
@Test public void skipNestedStructures() throws IOException {
JsonReader reader = new JsonReader("[[1,2,3],4]");
reader.beginArray();
reader.skipValue();
assertEquals("$[1]", reader.getPath());

File diff suppressed because it is too large Load diff

View file

@ -19,12 +19,36 @@ import java.io.IOException;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import junit.framework.TestCase;
import org.junit.Test;
@SuppressWarnings("resource")
public final class JsonWriterTest extends TestCase {
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public void testWrongTopLevelType() throws IOException {
public final class JsonWriterTest {
@Test public void nullsValuesNotSerializedByDefault() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
jsonWriter.name("a");
jsonWriter.nullValue();
jsonWriter.endObject();
jsonWriter.close();
assertEquals("{}", stringWriter.toString());
}
@Test public void nullsValuesSerializedWhenConfigured() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.setSerializeNulls(true);
jsonWriter.beginObject();
jsonWriter.name("a");
jsonWriter.nullValue();
jsonWriter.endObject();
jsonWriter.close();
assertEquals("{\"a\":null}", stringWriter.toString());
}
@Test public void wrongTopLevelType() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
try {
@ -34,7 +58,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testTwoNames() throws IOException {
@Test public void twoNames() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
@ -46,7 +70,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testNameWithoutValue() throws IOException {
@Test public void nameWithoutValue() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
@ -58,7 +82,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testValueWithoutName() throws IOException {
@Test public void valueWithoutName() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
@ -69,7 +93,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testMultipleTopLevelValues() throws IOException {
@Test public void multipleTopLevelValues() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray().endArray();
@ -80,7 +104,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testBadNestingObject() throws IOException {
@Test public void badNestingObject() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -92,7 +116,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testBadNestingArray() throws IOException {
@Test public void badNestingArray() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -104,7 +128,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testNullName() throws IOException {
@Test public void nullName() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
@ -115,9 +139,10 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testNullStringValue() throws IOException {
@Test public void nullStringValue() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.setSerializeNulls(true);
jsonWriter.beginObject();
jsonWriter.name("a");
jsonWriter.value((String) null);
@ -125,7 +150,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals("{\"a\":null}", stringWriter.toString());
}
public void testNonFiniteDoubles() throws IOException {
@Test public void nonFiniteDoubles() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -146,7 +171,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testNonFiniteBoxedDoubles() throws IOException {
@Test public void nonFiniteBoxedDoubles() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -167,7 +192,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testDoubles() throws IOException {
@Test public void doubles() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -193,7 +218,7 @@ public final class JsonWriterTest extends TestCase {
+ "2.718281828459045]", stringWriter.toString());
}
public void testLongs() throws IOException {
@Test public void longs() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -211,7 +236,7 @@ public final class JsonWriterTest extends TestCase {
+ "9223372036854775807]", stringWriter.toString());
}
public void testNumbers() throws IOException {
@Test public void numbers() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -227,7 +252,7 @@ public final class JsonWriterTest extends TestCase {
+ "3.141592653589793238462643383]", stringWriter.toString());
}
public void testBooleans() throws IOException {
@Test public void booleans() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -237,7 +262,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals("[true,false]", stringWriter.toString());
}
public void testNulls() throws IOException {
@Test public void nulls() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -246,7 +271,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals("[null]", stringWriter.toString());
}
public void testStrings() throws IOException {
@Test public void strings() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -289,7 +314,7 @@ public final class JsonWriterTest extends TestCase {
+ "\"\\u0019\"]", stringWriter.toString());
}
public void testUnicodeLineBreaksEscaped() throws IOException {
@Test public void unicodeLineBreaksEscaped() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -298,7 +323,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals("[\"\\u2028 \\u2029\"]", stringWriter.toString());
}
public void testEmptyArray() throws IOException {
@Test public void emptyArray() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -306,7 +331,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals("[]", stringWriter.toString());
}
public void testEmptyObject() throws IOException {
@Test public void emptyObject() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
@ -314,7 +339,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals("{}", stringWriter.toString());
}
public void testObjectsInArrays() throws IOException {
@Test public void objectsInArrays() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginArray();
@ -331,7 +356,7 @@ public final class JsonWriterTest extends TestCase {
+ "{\"c\":6,\"d\":true}]", stringWriter.toString());
}
public void testArraysInObjects() throws IOException {
@Test public void arraysInObjects() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
@ -350,7 +375,7 @@ public final class JsonWriterTest extends TestCase {
+ "\"b\":[6,true]}", stringWriter.toString());
}
public void testDeepNestingArrays() throws IOException {
@Test public void deepNestingArrays() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
for (int i = 0; i < 20; i++) {
@ -362,7 +387,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals("[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]", stringWriter.toString());
}
public void testDeepNestingObjects() throws IOException {
@Test public void deepNestingObjects() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
@ -379,7 +404,7 @@ public final class JsonWriterTest extends TestCase {
+ "}}}}}}}}}}}}}}}}}}}}}", stringWriter.toString());
}
public void testRepeatedName() throws IOException {
@Test public void repeatedName() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.beginObject();
@ -390,9 +415,10 @@ public final class JsonWriterTest extends TestCase {
assertEquals("{\"a\":true,\"a\":false}", stringWriter.toString());
}
public void testPrettyPrintObject() throws IOException {
@Test public void prettyPrintObject() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.setSerializeNulls(true);
jsonWriter.setIndent(" ");
jsonWriter.beginObject();
@ -427,7 +453,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals(expected, stringWriter.toString());
}
public void testPrettyPrintArray() throws IOException {
@Test public void prettyPrintArray() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new JsonWriter(stringWriter);
jsonWriter.setIndent(" ");
@ -464,7 +490,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals(expected, stringWriter.toString());
}
public void testLenientWriterPermitsMultipleTopLevelValues() throws IOException {
@Test public void lenientWriterPermitsMultipleTopLevelValues() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter writer = new JsonWriter(stringWriter);
writer.setLenient(true);
@ -476,7 +502,7 @@ public final class JsonWriterTest extends TestCase {
assertEquals("[][]", stringWriter.toString());
}
public void testStrictWriterDoesNotPermitMultipleTopLevelValues() throws IOException {
@Test public void strictWriterDoesNotPermitMultipleTopLevelValues() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter writer = new JsonWriter(stringWriter);
writer.beginArray();
@ -488,7 +514,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testClosedWriterThrowsOnStructure() throws IOException {
@Test public void closedWriterThrowsOnStructure() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter writer = new JsonWriter(stringWriter);
writer.beginArray();
@ -516,7 +542,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testClosedWriterThrowsOnName() throws IOException {
@Test public void closedWriterThrowsOnName() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter writer = new JsonWriter(stringWriter);
writer.beginArray();
@ -529,7 +555,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testClosedWriterThrowsOnValue() throws IOException {
@Test public void closedWriterThrowsOnValue() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter writer = new JsonWriter(stringWriter);
writer.beginArray();
@ -542,7 +568,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testClosedWriterThrowsOnFlush() throws IOException {
@Test public void closedWriterThrowsOnFlush() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter writer = new JsonWriter(stringWriter);
writer.beginArray();
@ -555,7 +581,7 @@ public final class JsonWriterTest extends TestCase {
}
}
public void testWriterCloseIsIdempotent() throws IOException {
@Test public void writerCloseIsIdempotent() throws IOException {
StringWriter stringWriter = new StringWriter();
JsonWriter writer = new JsonWriter(stringWriter);
writer.beginArray();