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 decodeObject(CharSequence input) { Object value = decode(input); if (value instanceof Map map) { @SuppressWarnings("unchecked") Map typed = (Map) map; return typed; } throw new IllegalArgumentException("JSON payload is not an object"); } public static List decodeArray(CharSequence input) { Object value = decode(input); if (value instanceof List list) { @SuppressWarnings("unchecked") List typed = (List) 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 readObject() { expect('{'); LinkedHashMap 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 readArray() { expect('['); ArrayList 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); } }