package immibis.bon.com.immibis.json; import java.io.EOFException; import java.io.IOException; import java.io.PushbackReader; import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; /** * Valid JSON values are Maps (from Strings to JSON values), Lists (of JSON values), Booleans, Strings, Doubles and null. */ public final class JsonReader { public static Object readJSON(Reader in) throws IOException { return readJSONInternal(new PushbackReader(in)); } private static char readNoWS(PushbackReader in) throws IOException { while(true) { char ch = read(in); if(ch != 0x20 && ch != 0x09 && ch != 0x0A && ch != 0x0D) return ch; } } private static char read(PushbackReader in) throws IOException { int ch = in.read(); if(ch == -1) throw new EOFException(); return (char)ch; } private static int fromHexChar(char ch) throws IOException { if(ch >= '0' && ch <= '9') return ch - '0'; if(ch >= 'A' && ch <= 'F') return ch - 'A' + 10; if(ch >= 'a' && ch <= 'f') return ch - 'a' + 10; throw new IOException("invalid hexadecimal character: "+ch); } private static String readString(PushbackReader in) throws IOException { StringBuilder sb = new StringBuilder(); if(readNoWS(in) != '"') throw new IOException("expected \" to start string"); while(true) { char ch = read(in); if(ch == '"') break; if(ch == '\\') { ch = read(in); switch(ch) { case '"': case '\\': case '/': sb.append(ch); break; case 'b': sb.append('\b'); break; case 'f': sb.append('\f'); break; case 'n': sb.append('\n'); break; case 'r': sb.append('\r'); break; case 't': sb.append('\t'); break; case 'u': int value = (fromHexChar(read(in)) << 12) | (fromHexChar(read(in)) << 8) | (fromHexChar(read(in)) << 4) | fromHexChar(read(in)); sb.append((char)value); break; default: throw new IOException("Invalid JSON string escape sequence: \\"+ch); } } else sb.append(ch); } return sb.toString(); } private static Object readJSONInternal(PushbackReader in) throws IOException { char firstChar = readNoWS(in); if(firstChar == 't') { if(read(in) != 'r' || read(in) != 'u' || read(in) != 'e') throw new IOException("expected 'rue' after 't'"); return Boolean.TRUE; } else if(firstChar == 'f') { if(read(in) != 'a' || read(in) != 'l' || read(in) != 's' || read(in) != 'e') throw new IOException("expected 'alse' after 'f'"); return Boolean.FALSE; } else if(firstChar == 'n') { if(read(in) != 'u' || read(in) != 'l' || read(in) != 'l') throw new IOException("expected 'ull' after 'n'"); return null; } else if(firstChar == '"') { in.unread(firstChar); return readString(in); } else if((firstChar >= '0' && firstChar <= '9') || firstChar == '-') { StringBuilder asString = new StringBuilder(); if(firstChar == '-') asString.append('-'); else in.unread(firstChar); char ch = read(in); asString.append(ch); if(ch == '0') { ch = read(in); } else { ch = read(in); while(ch >= '0' && ch <= '9') { asString.append(ch); ch = read(in); } } if(ch == '.') { asString.append(ch); ch = read(in); if(ch < '0' || ch > '9') throw new IOException("expected digits after ."); while(ch >= '0' && ch <= '9') { asString.append(ch); ch = read(in); } } if(ch == 'e' || ch == 'E') { asString.append(ch); ch = read(in); if(ch == '+' || ch == '-') { asString.append(ch); ch = read(in); } if(ch < '0' || ch > '9') throw new IOException("expected digits in exponent"); while(ch >= '0' && ch <= '9') { asString.append(ch); ch = read(in); } } in.unread(ch); return Double.parseDouble(asString.toString()); } else if(firstChar == '[') { ArrayList<Object> rv = new ArrayList<>(); char nextChar = readNoWS(in); if(nextChar == ']') return rv; in.unread(nextChar); while(true) { rv.add(readJSONInternal(in)); nextChar = readNoWS(in); if(nextChar == ']') break; else if(nextChar != ',') throw new IOException("expected ] or , after value in array"); } return rv; } else if(firstChar == '{') { HashMap<String, Object> rv = new HashMap<>(); char nextChar = readNoWS(in); if(nextChar == '}') return rv; in.unread(nextChar); while(true) { String name = readString(in); if(readNoWS(in) != ':') throw new IOException("expected : between name and value"); Object value = readJSONInternal(in); rv.put(name, value); nextChar = readNoWS(in); if(nextChar == '}') break; else if(nextChar != ',') throw new IOException("expected } or , after value in object"); } return rv; } else { throw new IOException("invalid start of json value: "+firstChar); } } }