From ac1b1027f8d36d0fcd05e311982625452cb948ac Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Sun, 10 Aug 2014 01:04:56 -0400 Subject: [PATCH] Use indexOfElement in JsonReader. This means we don't need to re-navigate through the segments for every single character. Should be a small performance win. --- moshi/pom.xml | 1 - .../java/com/squareup/moshi/JsonReader.java | 180 +++++++----------- .../com/squareup/moshi/JsonReaderTest.java | 15 +- pom.xml | 7 +- 4 files changed, 88 insertions(+), 115 deletions(-) diff --git a/moshi/pom.xml b/moshi/pom.xml index dde3e73..051d930 100644 --- a/moshi/pom.xml +++ b/moshi/pom.xml @@ -16,7 +16,6 @@ com.squareup.okio okio - 1.0.1 junit diff --git a/moshi/src/main/java/com/squareup/moshi/JsonReader.java b/moshi/src/main/java/com/squareup/moshi/JsonReader.java index 43a979c..af106ca 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonReader.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonReader.java @@ -20,6 +20,7 @@ import java.io.EOFException; import java.io.IOException; import okio.Buffer; import okio.BufferedSource; +import okio.ByteString; import okio.Source; /** @@ -176,6 +177,12 @@ import okio.Source; public class JsonReader implements Closeable { private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10; + private static final ByteString SINGLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("'\\"); + private static final ByteString DOUBLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("\"\\"); + private static final ByteString UNQUOTED_STRING_TERMINALS + = ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#="); + private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r"); + private static final int PEEKED_NONE = 0; private static final int PEEKED_BEGIN_OBJECT = 1; private static final int PEEKED_END_OBJECT = 2; @@ -271,7 +278,7 @@ public class JsonReader implements Closeable { */ public JsonReader(String s) { this.source = new Buffer().writeUtf8(s); - this.buffer = new Buffer(); + this.buffer = source.buffer(); } /** @@ -561,7 +568,7 @@ public class JsonReader implements Closeable { buffer.readByte(); // Consume '['. return peeked = PEEKED_BEGIN_ARRAY; case '{': - buffer.readByte(); // Consume ']'. + buffer.readByte(); // Consume '{'. return peeked = PEEKED_BEGIN_OBJECT; default: } @@ -760,10 +767,10 @@ public class JsonReader implements Closeable { String result; if (p == PEEKED_UNQUOTED_NAME) { result = nextUnquotedValue(); - } else if (p == PEEKED_SINGLE_QUOTED_NAME) { - result = nextQuotedValue('\''); } else if (p == PEEKED_DOUBLE_QUOTED_NAME) { - result = nextQuotedValue('"'); + result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); + } else if (p == PEEKED_SINGLE_QUOTED_NAME) { + result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); } else { throw new IllegalStateException("Expected a name but was " + peek() + " at path " + getPath()); @@ -789,10 +796,10 @@ public class JsonReader implements Closeable { String result; if (p == PEEKED_UNQUOTED) { result = nextUnquotedValue(); - } else if (p == PEEKED_SINGLE_QUOTED) { - result = nextQuotedValue('\''); } else if (p == PEEKED_DOUBLE_QUOTED) { - result = nextQuotedValue('"'); + result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); + } else if (p == PEEKED_SINGLE_QUOTED) { + result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); } else if (p == PEEKED_BUFFERED) { result = peekedString; peekedString = null; @@ -878,8 +885,10 @@ public class JsonReader implements Closeable { if (p == PEEKED_NUMBER) { peekedString = buffer.readUtf8(peekedNumberLength); - } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) { - peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); + } else if (p == PEEKED_DOUBLE_QUOTED) { + peekedString = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); + } else if (p == PEEKED_SINGLE_QUOTED) { + peekedString = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); } else if (p == PEEKED_UNQUOTED) { peekedString = nextUnquotedValue(); } else if (p != PEEKED_BUFFERED) { @@ -923,8 +932,10 @@ public class JsonReader implements Closeable { if (p == PEEKED_NUMBER) { peekedString = buffer.readUtf8(peekedNumberLength); - } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) { - peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); + } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED) { + peekedString = p == PEEKED_DOUBLE_QUOTED + ? nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) + : nextQuotedValue(SINGLE_QUOTE_OR_SLASH); try { long result = Long.parseLong(peekedString); peeked = PEEKED_NONE; @@ -957,109 +968,61 @@ public class JsonReader implements Closeable { * 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 nextQuotedValue(char quote) throws IOException { - StringBuilder builder = new StringBuilder(); - int p = 0; - while (fillBuffer(p + 1)) { - int c = buffer.getByte(p++); + private String nextQuotedValue(ByteString runTerminator) throws IOException { + StringBuilder builder = null; + while (true) { + long index = source.indexOfElement(runTerminator); + if (index == -1L) 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)); + // If we've got an escape character, we're going to need a string builder. + if (buffer.getByte(index) == '\\') { + if (builder == null) builder = new StringBuilder(); + builder.append(buffer.readUtf8(index)); buffer.readByte(); // '\' builder.append(readEscapeCharacter()); - p = 0; + continue; + } + + // If it isn't the escape character, it's the quote. Return the string. + if (builder == null) { + String result = buffer.readUtf8(index); + buffer.readByte(); // Consume the quote character. + return result; + } else { + builder.append(buffer.readUtf8(index)); + buffer.readByte(); // Consume the quote character. + return builder.toString(); } } - - throw syntaxError("Unterminated string"); } - /** - * Returns an unquoted value as a string. - */ - @SuppressWarnings("fallthrough") + /** Returns an unquoted value as a string. */ private String nextUnquotedValue() throws IOException { - 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; - } - } - - return buffer.readUtf8(i); + long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS); + return i != -1 ? buffer.readUtf8(i) : buffer.readUtf8(); } - private void skipQuotedValue(char quote) throws IOException { - 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); + private void skipQuotedValue(ByteString runTerminator) throws IOException { + while (true) { + long index = source.indexOfElement(runTerminator); + if (index == -1L) throw syntaxError("Unterminated string"); + + if (buffer.getByte(index) == '\\') { + buffer.skip(index + 1); readEscapeCharacter(); - p = 0; + } else { + buffer.skip(index + 1); + return; } } - throw syntaxError("Unterminated string"); } private void skipUnquotedValue() throws IOException { - 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; - } - } - - buffer.skip(i); + long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS); + buffer.skip(i != -1L ? i : buffer.size()); } /** @@ -1092,8 +1055,10 @@ public class JsonReader implements Closeable { if (p == PEEKED_NUMBER) { peekedString = buffer.readUtf8(peekedNumberLength); - } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) { - peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); + } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED) { + peekedString = p == PEEKED_DOUBLE_QUOTED + ? nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) + : nextQuotedValue(SINGLE_QUOTE_OR_SLASH); try { result = Integer.parseInt(peekedString); peeked = PEEKED_NONE; @@ -1158,10 +1123,10 @@ public class JsonReader implements Closeable { count--; } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) { skipUnquotedValue(); - } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { - skipQuotedValue('\''); } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) { - skipQuotedValue('"'); + skipQuotedValue(DOUBLE_QUOTE_OR_SLASH); + } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { + skipQuotedValue(SINGLE_QUOTE_OR_SLASH); } else if (p == PEEKED_NUMBER) { buffer.skip(peekedNumberLength); } @@ -1193,10 +1158,7 @@ public class JsonReader implements Closeable { * false. */ private boolean fillBuffer(int minimum) throws IOException { - while (buffer.size() < minimum) { - if (source.read(buffer, 2048) == -1) return false; - } - return true; + return source.request(minimum); } /** @@ -1285,12 +1247,8 @@ public class JsonReader implements Closeable { * caller. */ private void skipToEndOfLine() throws IOException { - while (fillBuffer(1)) { - byte c = buffer.readByte(); - if (c == '\n' || c == '\r') { - break; - } - } + long index = source.indexOfElement(LINEFEED_OR_CARRIAGE_RETURN); + buffer.skip(index != -1 ? index + 1 : buffer.size()); } /** diff --git a/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java b/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java index 559da18..3e5a5ba 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java @@ -17,8 +17,6 @@ package com.squareup.moshi; import java.io.EOFException; import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; import java.util.Arrays; import okio.Source; import org.junit.Ignore; @@ -895,6 +893,11 @@ public final class JsonReaderTest { reader.setLenient(true); reader.beginArray(); assertEquals(true, reader.nextBoolean()); + + reader = new JsonReader("a//"); + reader.setLenient(true); + assertEquals("a", reader.nextString()); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); } @Test public void strictCommentsWithSkipValue() throws IOException { @@ -1011,6 +1014,14 @@ public final class JsonReaderTest { assertEquals("a", reader.nextString()); } + @Test public void lenientUnquotedStringsDelimitedByComment() throws IOException { + JsonReader reader = new JsonReader("[a#comment\n]"); + reader.setLenient(true); + reader.beginArray(); + assertEquals("a", reader.nextString()); + reader.endArray(); + } + @Test public void strictSingleQuotedStrings() throws IOException { JsonReader reader = new JsonReader("['a']"); reader.beginArray(); diff --git a/pom.xml b/pom.xml index 0a21dfb..f305da8 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ 1.6 - 1.0.1 + 1.0.2-SNAPSHOT 4.11 @@ -58,6 +58,11 @@ junit ${junit.version} + + com.squareup.okio + okio + ${okio.version} +