Last active
July 22, 2024 20:25
-
-
Save apangin/c30d6737c4ae0afe8811d5879dbad287 to your computer and use it in GitHub Desktop.
Simplified JSON reader
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
/* | |
* Copyright Andrei Pangin | |
* SPDX-License-Identifier: Apache-2.0 | |
*/ | |
import java.util.ArrayList; | |
import java.util.LinkedHashMap; | |
import java.util.Map; | |
/** | |
* Beware: this is NOT a complete and fully compliant JSON parser! | |
* Its main purpose is to decode typical simple queries without third-party dependencies. | |
*/ | |
public class JsonReader { | |
protected byte[] array; | |
protected int offset; | |
protected int next; | |
public JsonReader(byte[] array) { | |
this(array, 0); | |
} | |
public JsonReader(byte[] array, int offset) { | |
this.array = array; | |
this.offset = offset; | |
skipWhitespace(); | |
} | |
protected int read() { | |
int b = next; | |
next = offset < array.length ? array[offset++] & 0xff : -1; | |
return b; | |
} | |
public final int next() { | |
return next; | |
} | |
public final int skipWhitespace() { | |
while (next <= ' ' && next != -1) { | |
read(); | |
} | |
return next; | |
} | |
public final IllegalArgumentException exception(String message) { | |
return new IllegalArgumentException(message + " at " + offset); | |
} | |
public final void expect(int b, String message) { | |
if (read() != b) { | |
throw exception(message); | |
} | |
} | |
public final boolean readBoolean() { | |
int b = read(); | |
if (b == 't' && read() == 'r' && read() == 'u' && read() == 'e') { | |
return true; | |
} else if (b == 'f' && read() == 'a' && read() == 'l' && read() == 's' && read() == 'e') { | |
return false; | |
} | |
throw exception("Expected boolean"); | |
} | |
public final byte readByte() { | |
return Byte.parseByte(readNumber()); | |
} | |
public final short readShort() { | |
return Short.parseShort(readNumber()); | |
} | |
public final char readChar() { | |
return readString().charAt(0); | |
} | |
public final int readInt() { | |
return Integer.parseInt(readNumber()); | |
} | |
public final long readLong() { | |
return Long.parseLong(readNumber()); | |
} | |
public float readFloat() { | |
return Float.parseFloat(readNumber()); | |
} | |
public final double readDouble() { | |
return Double.parseDouble(readNumber()); | |
} | |
public final String readNumber() { | |
StringBuilder sb = new StringBuilder(); | |
// Sign | |
if (next == '-') { | |
sb.append((char) read()); | |
} | |
// Integer | |
int nochars = sb.length(); | |
while (next >= '0' && next <= '9') { | |
sb.append((char) read()); | |
} | |
// Fraction | |
if (next == '.') { | |
sb.append((char) read()); | |
if (!(next >= '0' && next <= '9')) throw exception("Expected number"); | |
do { | |
sb.append((char) read()); | |
} while (next >= '0' && next <= '9'); | |
} | |
if (sb.length() <= nochars) { | |
throw exception("Expected number"); | |
} | |
// Exponent | |
if (next == 'e' || next == 'E') { | |
sb.append((char) read()); | |
if (next == '-' || next == '+') { | |
sb.append((char) read()); | |
} | |
if (!(next >= '0' && next <= '9')) throw exception("Expected number"); | |
do { | |
sb.append((char) read()); | |
} while (next >= '0' && next <= '9'); | |
} | |
return sb.toString(); | |
} | |
public final int readHexChar() { | |
int b = read(); | |
if (b >= '0' && b <= '9') { | |
return b - '0'; | |
} else if (b >= 'A' && b <= 'F') { | |
return b - 'A'; | |
} else if (b >= 'a' && b <= 'f') { | |
return b - 'a'; | |
} | |
throw exception("Invalid escape character"); | |
} | |
public final char readEscapeChar() { | |
int b = read(); | |
switch (b) { | |
case 'b': | |
return '\b'; | |
case 'f': | |
return '\f'; | |
case 'n': | |
return '\n'; | |
case 'r': | |
return '\r'; | |
case 't': | |
return '\t'; | |
case 'u': | |
return (char) (readHexChar() << 12 | readHexChar() << 8 | readHexChar() << 4 | readHexChar()); | |
default: | |
return (char) b; | |
} | |
} | |
public Object readNull() { | |
if (read() != 'n' || read() != 'u' || read() != 'l' || read() != 'l') { | |
throw exception("Expected null"); | |
} | |
return null; | |
} | |
public String readString() { | |
StringBuilder sb = new StringBuilder(); | |
expect('\"', "Expected string"); | |
while (next >= 0 && next != '\"') { | |
int b = read(); | |
if ((b & 0x80) == 0) { | |
sb.append(b == '\\' ? readEscapeChar() : (char) b); | |
} else if ((b & 0xe0) == 0xc0) { | |
sb.append((char) ((b & 0x1f) << 6 | (read() & 0x3f))); | |
} else if ((b & 0xf0) == 0xe0) { | |
sb.append((char) ((b & 0x0f) << 12 | (read() & 0x3f) << 6 | (read() & 0x3f))); | |
} else { | |
int v = (b & 0x07) << 18 | (read() & 0x3f) << 12 | (read() & 0x3f) << 6 | (read() & 0x3f); | |
sb.append((char) (0xd800 | (v - 0x10000) >>> 10)).append((char) (0xdc00 | (v & 0x3ff))); | |
} | |
} | |
expect('\"', "Unexpected end of string"); | |
return sb.toString(); | |
} | |
public ArrayList<Object> readArray() { | |
ArrayList<Object> result = new ArrayList<>(); | |
expect('[', "Expected array"); | |
for (boolean needComma = false; skipWhitespace() != ']'; needComma = true) { | |
if (needComma) { | |
expect(',', "Unexpected end of array"); | |
skipWhitespace(); | |
} | |
result.add(readObject()); | |
} | |
read(); | |
return result; | |
} | |
public Map<String, Object> readMap() { | |
LinkedHashMap<String, Object> result = new LinkedHashMap<>(); | |
expect('{', "Expected map"); | |
for (boolean needComma = false; skipWhitespace() != '}'; needComma = true) { | |
if (needComma) { | |
expect(',', "Unexpected end of map"); | |
skipWhitespace(); | |
} | |
String key = readString(); | |
skipWhitespace(); | |
expect(':', "Expected key-value pair"); | |
skipWhitespace(); | |
result.put(key, readObject()); | |
} | |
read(); | |
return result; | |
} | |
public Object readObject() { | |
switch (next) { | |
case 'n': | |
return readNull(); | |
case 'f': | |
case 't': | |
return readBoolean(); | |
case '\"': | |
return readString(); | |
case '[': | |
return readArray(); | |
case '{': | |
return readMap(); | |
case '0': | |
case '1': | |
case '2': | |
case '3': | |
case '4': | |
case '5': | |
case '6': | |
case '7': | |
case '8': | |
case '9': | |
case '-': | |
case '.': | |
return parseNumber(readNumber()); | |
} | |
throw exception("Expected JSON object"); | |
} | |
private static Number parseNumber(String number) { | |
int length = number.length(); | |
for (int i = 0; i < length; i++) { | |
char c = number.charAt(i); | |
if (c == '.' || c > '9') { | |
return Double.parseDouble(number); | |
} | |
} | |
long n = Long.parseLong(number); | |
return n == (int) n ? (int) n : (Number) n; | |
} | |
} |
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
import java.nio.charset.StandardCharsets; | |
public class JsonReaderTest { | |
static final String SAMPLE = "[{\n" + | |
" \"created_at\": \"Thu Jun 22 21:00:00 +0000 2017\",\n" + | |
" \"id\": 877994604561387500,\n" + | |
" \"id_str\": \"877994604561387520\",\n" + | |
" \"text\": \"Creating a Grocery List Manager \\u0026 Display Items https://t.co/xFox12345 #Angular\",\n" + | |
" \"truncated\": false,\n" + | |
" \"entities\": {\n" + | |
" \"hashtags\": [{\n" + | |
" \"text\": \"Angular\",\n" + | |
" \"indices\": [103, 111]\n" + | |
" }],\n" + | |
" \"symbols\": [null],\n" + | |
" \"user_mentions\": [],\n" + | |
" \"urls\": [{\n" + | |
" \"url\": \"https://t.co/xFox12345\",\n" + | |
" \"expanded_url\": \"http://example.com/2sr60pf\",\n" + | |
" \"display_url\": \"example.com/2sr60pf\",\n" + | |
" \"indices\": [79, 102]\n" + | |
" }]\n" + | |
" },\n" + | |
" \"source\": \"<a href=\\\"http://example.com\\\" rel=\\\"nofollow\\\">Some link</a>\",\n" + | |
" \"user\": {\n" + | |
" \"id\": 772682964,\n" + | |
" \"id_str\": \"772682964\",\n" + | |
" \"name\": \"Example JavaScript\",\n" + | |
" \"screen_name\": \"ExampleJS\",\n" + | |
" \"location\": \"Melbourne, Australia\",\n" + | |
" \"description\": \"Keep up with JavaScript tutorials, tips, tricks and articles.\",\n" + | |
" \"url\": \"http://t.co/cCHxxxxx\",\n" + | |
" \"entities\": {\n" + | |
" \"url\": {\n" + | |
" \"urls\": [{\n" + | |
" \"url\": \"http://t.co/cCHxxxxx\",\n" + | |
" \"expanded_url\": \"http://example.com/javascript\",\n" + | |
" \"display_url\": \"example.com/javascript\",\n" + | |
" \"indices\": [0, 22]\n" + | |
" }]\n" + | |
" },\n" + | |
" \"description\": {\n" + | |
" \"urls\": []\n" + | |
" }\n" + | |
" },\n" + | |
" \"protected\": false,\n" + | |
" \"followers_count\": 2145,\n" + | |
" \"friends_count\": 18,\n" + | |
" \"listed_count\": 328,\n" + | |
" \"created_at\": \"Wed Aug 22 02:06:33 +0000 2012\",\n" + | |
" \"favourites_count\": 57,\n" + | |
" \"utc_offset\": 43200,\n" + | |
" \"time_zone\": \"Wellington\"\n" + | |
" }\n" + | |
"}]"; | |
public static void main(String[] args) throws Exception { | |
Object o = new JsonReader(SAMPLE.getBytes(StandardCharsets.UTF_8)).readObject(); | |
System.out.println(o); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment