-
-
Notifications
You must be signed in to change notification settings - Fork 481
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: optimisation - use char arrays in JSON parser instead of lists
- Loading branch information
Ronald Holshausen
committed
Jun 16, 2020
1 parent
d87fef5
commit 4d67a8c
Showing
8 changed files
with
344 additions
and
229 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
core/support/src/main/java/au/com/dius/pact/core/support/json/BaseJsonLexer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package au.com.dius.pact.core.support.json; | ||
|
||
import com.github.michaelbull.result.Err; | ||
import com.github.michaelbull.result.Ok; | ||
import com.github.michaelbull.result.Result; | ||
import org.apache.commons.lang3.ArrayUtils; | ||
|
||
import java.util.function.Predicate; | ||
|
||
public class BaseJsonLexer { | ||
protected JsonSource json; | ||
|
||
public BaseJsonLexer(JsonSource json) { | ||
this.json = json; | ||
} | ||
|
||
protected void skipWhitespace() { | ||
Character next = json.peekNextChar(); | ||
while (next != null && Character.isWhitespace(next)) { | ||
json.advance(); | ||
next = json.peekNextChar(); | ||
} | ||
} | ||
|
||
protected Result<JsonToken.StringValue, JsonException> scanString() { | ||
char[] buffer = new char[128]; | ||
int index = 0; | ||
Character next; | ||
do { | ||
next = json.nextChar(); | ||
if (next != null && next == '\\') { | ||
Character escapeCode = json.nextChar(); | ||
switch (escapeCode) { | ||
case '"': if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = '"'; break; | ||
case '\\': if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = '\\'; break; | ||
case '/': if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = '/'; break; | ||
case 'b': if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = '\b'; break; | ||
case 'f': if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = '\u000c'; break; | ||
case 'n': if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = '\n'; break; | ||
case 'r': if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = '\r'; break; | ||
case 't': if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = '\t'; break; | ||
case 'u': { | ||
Character u1 = json.nextChar(); | ||
if (u1 == null) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), Unicode characters require 4 hex digits", json.documentPointer()))); | ||
} else if (invalidHex(u1)) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), '%c' is not a valid hex code character", json.documentPointer(), u1))); | ||
} | ||
Character u2 = json.nextChar(); | ||
if (u2 == null) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), Unicode characters require 4 hex digits", json.documentPointer()))); | ||
} else if (invalidHex(u2)) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), '%c' is not a valid hex code character", json.documentPointer(), u2))); | ||
} | ||
Character u3 = json.nextChar(); | ||
if (u3 == null) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), Unicode characters require 4 hex digits", json.documentPointer()))); | ||
} else if (invalidHex(u3)) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), '%c' is not a valid hex code character", json.documentPointer(), u3))); | ||
} | ||
Character u4 = json.nextChar(); | ||
if (u4 == null) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), Unicode characters require 4 hex digits", json.documentPointer()))); | ||
} else if (invalidHex(u4)) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), '%c' is not a valid hex code character", json.documentPointer(), u4))); | ||
} | ||
int hex = Integer.parseInt(new String(new char[]{u1, u2, u3, u4}), 16); | ||
if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = (char) hex; | ||
break; | ||
} | ||
default: return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), '%c' is not a valid escape code", json.documentPointer(), escapeCode))); | ||
} | ||
} else if (next == null) { | ||
return new Err(new JsonException(String.format("Invalid JSON (%s), End of document scanning for string terminator", | ||
json.documentPointer()))); | ||
} else if (next != '"') { | ||
if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = next; | ||
} | ||
} while (next != '"'); | ||
return new Ok(new JsonToken.StringValue(ArrayUtils.subarray(buffer, 0, index))); | ||
} | ||
|
||
private char[] allocate(char[] buffer) { | ||
return allocate(buffer, 1); | ||
} | ||
|
||
private char[] allocate(char[] buffer, int size) { | ||
char[] newBuffer = new char[Math.max(buffer.length * 2, size)]; | ||
System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); | ||
return newBuffer; | ||
} | ||
|
||
private boolean invalidHex(Character ch) { | ||
if (Character.isDigit(ch)) { | ||
return false; | ||
} else { | ||
switch (ch) { | ||
case 'a': | ||
case 'b': | ||
case 'c': | ||
case 'd': | ||
case 'e': | ||
case 'f': | ||
case 'A': | ||
case 'B': | ||
case 'C': | ||
case 'D': | ||
case 'E': | ||
case 'F': | ||
return false; | ||
default: | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
protected char[] consumeChars(Character first, Predicate<Character> predicate) { | ||
char[] buffer = new char[16]; | ||
buffer[0] = first; | ||
int index = 1; | ||
Character next = json.peekNextChar(); | ||
while (next != null && predicate.test(next)) { | ||
if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = next; | ||
json.advance(); | ||
next = json.peekNextChar(); | ||
} | ||
return ArrayUtils.subarray(buffer, 0, index); | ||
} | ||
|
||
protected Result<JsonToken, JsonException> scanNumber(Character next) { | ||
char[] buffer = consumeChars(next, Character::isDigit); | ||
if (next == '-' && buffer.length == 1) { | ||
return new Err(new JsonException(String.format( | ||
"Invalid JSON (%s), found a '%c' that was not followed by any digits", json.documentPointer(), next))); | ||
} | ||
Character ch = json.peekNextChar(); | ||
if (ch != null && (ch == '.' || ch == 'e' || ch == 'E')) { | ||
return scanDecimalNumber(buffer); | ||
} else { | ||
return new Ok(new JsonToken.Integer(buffer)); | ||
} | ||
} | ||
|
||
protected Result<JsonToken, JsonException> scanDecimalNumber(char[] buffer) { | ||
int index = buffer.length; | ||
Character next = json.peekNextChar(); | ||
if (next != null && next == '.') { | ||
char[] digits = consumeChars(json.nextChar(), Character::isDigit); | ||
buffer = allocate(buffer, digits.length); | ||
System.arraycopy(digits, 0, buffer, index, digits.length); | ||
index += digits.length; | ||
if (!Character.isDigit(buffer[index - 1])) { | ||
return new Err(new JsonException(String.format("Invalid JSON (%s), '%s' is not a valid number", | ||
json.documentPointer(), new String(ArrayUtils.subarray(buffer, 0, index))))); | ||
} | ||
next = json.peekNextChar(); | ||
} | ||
if (next != null && (next == 'e' || next == 'E')) { | ||
if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = json.nextChar(); | ||
next = json.peekNextChar(); | ||
if (next != null && (next == '+' || next == '-')) { | ||
if (index >= buffer.length) { buffer = allocate(buffer); }; buffer[index++] = json.nextChar(); | ||
} | ||
char[] digits = consumeChars(json.nextChar(), Character::isDigit); | ||
buffer = allocate(buffer, digits.length); | ||
System.arraycopy(digits, 0, buffer, index, digits.length); | ||
index += digits.length; | ||
if (!Character.isDigit(buffer[index - 1])) { | ||
return new Err(new JsonException(String.format("Invalid JSON (%s), '%s' is not a valid number", | ||
json.documentPointer(), new String(ArrayUtils.subarray(buffer, 0, index))))); | ||
} | ||
} | ||
return new Ok(new JsonToken.Decimal(ArrayUtils.subarray(buffer, 0, index))); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
core/support/src/main/java/au/com/dius/pact/core/support/json/InputStreamSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package au.com.dius.pact.core.support.json; | ||
|
||
import java.io.InputStream; | ||
import java.io.InputStreamReader; | ||
|
||
public class InputStreamSource extends ReaderSource { | ||
public InputStreamSource(InputStream source) { | ||
super(new InputStreamReader(source)); | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
core/support/src/main/java/au/com/dius/pact/core/support/json/JsonSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package au.com.dius.pact.core.support.json; | ||
|
||
public abstract class JsonSource { | ||
public abstract Character nextChar(); | ||
public abstract Character peekNextChar(); | ||
public abstract void advance(int count); | ||
|
||
protected long line = 0; | ||
protected long character = 0; | ||
|
||
public void advance() { | ||
advance(1); | ||
} | ||
|
||
public String documentPointer() { | ||
return String.format("%d:%d", line + 1, character + 1); | ||
} | ||
} |
77 changes: 77 additions & 0 deletions
77
core/support/src/main/java/au/com/dius/pact/core/support/json/ReaderSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package au.com.dius.pact.core.support.json; | ||
|
||
import java.io.IOException; | ||
import java.io.Reader; | ||
|
||
public class ReaderSource extends JsonSource { | ||
private Reader reader; | ||
private Character buffer = null; | ||
|
||
public ReaderSource(Reader reader) { | ||
this.reader = reader; | ||
} | ||
|
||
public Character nextChar() { | ||
if (buffer != null) { | ||
Character c = buffer; | ||
buffer = null; | ||
return c; | ||
} else { | ||
int next = 0; | ||
try { | ||
next = reader.read(); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
if (next == -1) { | ||
return null; | ||
} else { | ||
if (next == '\n') { | ||
character = 0; | ||
line++; | ||
} else { | ||
character++; | ||
} | ||
return (char) next; | ||
} | ||
} | ||
} | ||
|
||
public Character peekNextChar() { | ||
if (buffer == null) { | ||
int next = 0; | ||
try { | ||
next = reader.read(); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
if (next == -1) { | ||
buffer = null; | ||
} else { | ||
buffer = (char) next; | ||
} | ||
} | ||
return buffer; | ||
} | ||
|
||
public void advance(int count) { | ||
int charsToSkip = count; | ||
if (buffer != null) { | ||
buffer = null; | ||
charsToSkip = count - 1; | ||
} | ||
try { | ||
for (int i = 0; i < charsToSkip; i++) { | ||
int next = reader.read(); | ||
if (next == '\n') { | ||
character = 0; | ||
line++; | ||
} else { | ||
character++; | ||
} | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
core/support/src/main/java/au/com/dius/pact/core/support/json/StringSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package au.com.dius.pact.core.support.json; | ||
|
||
public class StringSource extends JsonSource { | ||
private char[] json; | ||
private int index = 0; | ||
|
||
public StringSource(char[] json) { | ||
this.json = json; | ||
} | ||
|
||
public Character nextChar() { | ||
Character c = peekNextChar(); | ||
if (c != null) { | ||
if (c == '\n') { | ||
character = 0; | ||
line++; | ||
} else { | ||
character++; | ||
} | ||
index++; | ||
} | ||
return c; | ||
} | ||
|
||
public Character peekNextChar() { | ||
if (index >= json.length) { | ||
return null; | ||
} else { | ||
return json[index]; | ||
} | ||
} | ||
|
||
public void advance(int count) { | ||
for (int i = 0; i < count; i++) { | ||
char next = json[index++]; | ||
if (next == '\n') { | ||
character = 0; | ||
line++; | ||
} else { | ||
character++; | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.