267 lines
7.3 KiB
Java
267 lines
7.3 KiB
Java
package com.reliancy.io;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Lightweight JSON decoder that produces common Java object trees.
|
|
*/
|
|
public final class JsonDecoder {
|
|
private final CharSequence input;
|
|
private int index;
|
|
|
|
private JsonDecoder(CharSequence input) {
|
|
this.input = input == null ? "" : input;
|
|
}
|
|
|
|
public static Object decode(CharSequence input) {
|
|
JsonDecoder parser = new JsonDecoder(input);
|
|
Object value = parser.readValue();
|
|
parser.skipWhitespace();
|
|
if (parser.hasMore()) {
|
|
throw parser.error("Unexpected trailing content");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
public static Map<String, Object> decodeObject(CharSequence input) {
|
|
Object value = decode(input);
|
|
if (value instanceof Map<?, ?> map) {
|
|
@SuppressWarnings("unchecked")
|
|
Map<String, Object> typed = (Map<String, Object>) map;
|
|
return typed;
|
|
}
|
|
throw new IllegalArgumentException("JSON payload is not an object");
|
|
}
|
|
|
|
public static List<Object> decodeArray(CharSequence input) {
|
|
Object value = decode(input);
|
|
if (value instanceof List<?> list) {
|
|
@SuppressWarnings("unchecked")
|
|
List<Object> typed = (List<Object>) list;
|
|
return typed;
|
|
}
|
|
throw new IllegalArgumentException("JSON payload is not an array");
|
|
}
|
|
|
|
private Object readValue() {
|
|
skipWhitespace();
|
|
if (!hasMore()) {
|
|
throw error("Unexpected end of input");
|
|
}
|
|
char ch = current();
|
|
switch (ch) {
|
|
case '{':
|
|
return readObject();
|
|
case '[':
|
|
return readArray();
|
|
case '"':
|
|
return readString();
|
|
case 't':
|
|
expectLiteral("true");
|
|
return Boolean.TRUE;
|
|
case 'f':
|
|
expectLiteral("false");
|
|
return Boolean.FALSE;
|
|
case 'n':
|
|
expectLiteral("null");
|
|
return null;
|
|
default:
|
|
if (ch == '-' || Character.isDigit(ch)) {
|
|
return readNumber();
|
|
}
|
|
throw error("Unexpected token");
|
|
}
|
|
}
|
|
|
|
private Map<String, Object> readObject() {
|
|
expect('{');
|
|
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
|
|
skipWhitespace();
|
|
if (peek('}')) {
|
|
index++;
|
|
return result;
|
|
}
|
|
while (true) {
|
|
skipWhitespace();
|
|
String key = readString();
|
|
skipWhitespace();
|
|
expect(':');
|
|
Object value = readValue();
|
|
result.put(key, value);
|
|
skipWhitespace();
|
|
if (peek('}')) {
|
|
index++;
|
|
return result;
|
|
}
|
|
expect(',');
|
|
}
|
|
}
|
|
|
|
private List<Object> readArray() {
|
|
expect('[');
|
|
ArrayList<Object> result = new ArrayList<>();
|
|
skipWhitespace();
|
|
if (peek(']')) {
|
|
index++;
|
|
return result;
|
|
}
|
|
while (true) {
|
|
result.add(readValue());
|
|
skipWhitespace();
|
|
if (peek(']')) {
|
|
index++;
|
|
return result;
|
|
}
|
|
expect(',');
|
|
}
|
|
}
|
|
|
|
private String readString() {
|
|
expect('"');
|
|
StringBuilder buf = new StringBuilder();
|
|
while (hasMore()) {
|
|
char ch = input.charAt(index++);
|
|
if (ch == '"') {
|
|
return buf.toString();
|
|
}
|
|
if (ch != '\\') {
|
|
buf.append(ch);
|
|
continue;
|
|
}
|
|
if (!hasMore()) {
|
|
throw error("Unterminated escape sequence");
|
|
}
|
|
char esc = input.charAt(index++);
|
|
switch (esc) {
|
|
case '"':
|
|
case '\\':
|
|
case '/':
|
|
buf.append(esc);
|
|
break;
|
|
case 'b':
|
|
buf.append('\b');
|
|
break;
|
|
case 'f':
|
|
buf.append('\f');
|
|
break;
|
|
case 'n':
|
|
buf.append('\n');
|
|
break;
|
|
case 'r':
|
|
buf.append('\r');
|
|
break;
|
|
case 't':
|
|
buf.append('\t');
|
|
break;
|
|
case 'u':
|
|
buf.append(readUnicodeEscape());
|
|
break;
|
|
default:
|
|
throw error("Unsupported escape sequence");
|
|
}
|
|
}
|
|
throw error("Unterminated string");
|
|
}
|
|
|
|
private Number readNumber() {
|
|
int start = index;
|
|
if (peek('-')) {
|
|
index++;
|
|
}
|
|
readDigits();
|
|
boolean fractional = false;
|
|
if (peek('.')) {
|
|
fractional = true;
|
|
index++;
|
|
readDigits();
|
|
}
|
|
if (peek('e') || peek('E')) {
|
|
fractional = true;
|
|
index++;
|
|
if (peek('+') || peek('-')) {
|
|
index++;
|
|
}
|
|
readDigits();
|
|
}
|
|
String token = input.subSequence(start, index).toString();
|
|
if (!Handy.isNumeric(token)) {
|
|
throw error("Invalid number");
|
|
}
|
|
if (fractional) {
|
|
return Double.valueOf(token);
|
|
}
|
|
try {
|
|
return Integer.valueOf(token);
|
|
} catch (NumberFormatException ignore) {
|
|
return Long.valueOf(token);
|
|
}
|
|
}
|
|
|
|
private char readUnicodeEscape() {
|
|
if (index + 4 > input.length()) {
|
|
throw error("Invalid unicode escape");
|
|
}
|
|
int value = 0;
|
|
for (int i = 0; i < 4; i++) {
|
|
char ch = input.charAt(index++);
|
|
int digit = Character.digit(ch, 16);
|
|
if (digit < 0) {
|
|
throw error("Invalid unicode escape");
|
|
}
|
|
value = (value << 4) + digit;
|
|
}
|
|
return (char) value;
|
|
}
|
|
|
|
private void readDigits() {
|
|
int start = index;
|
|
while (hasMore() && Character.isDigit(current())) {
|
|
index++;
|
|
}
|
|
if (start == index) {
|
|
throw error("Expected digit");
|
|
}
|
|
}
|
|
|
|
private void expectLiteral(String literal) {
|
|
for (int i = 0; i < literal.length(); i++) {
|
|
if (!hasMore() || input.charAt(index++) != literal.charAt(i)) {
|
|
throw error("Expected literal " + literal);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void expect(char expected) {
|
|
skipWhitespace();
|
|
if (!peek(expected)) {
|
|
throw error("Expected '" + expected + "'");
|
|
}
|
|
index++;
|
|
}
|
|
|
|
private void skipWhitespace() {
|
|
while (hasMore() && Character.isWhitespace(current())) {
|
|
index++;
|
|
}
|
|
}
|
|
|
|
private boolean hasMore() {
|
|
return index < input.length();
|
|
}
|
|
|
|
private boolean peek(char ch) {
|
|
return hasMore() && current() == ch;
|
|
}
|
|
|
|
private char current() {
|
|
return input.charAt(index);
|
|
}
|
|
|
|
private IllegalArgumentException error(String message) {
|
|
return new IllegalArgumentException(message + " at offset " + index);
|
|
}
|
|
}
|