package com.minamp.util; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * Tiny JSON reader/writer. Plex responses are parsed into {@link Obj}/{@link Arr}, which offer * forgiving opt* accessors (Plex is loose about numbers vs strings). */ public final class Json { private Json() {} public static final class Obj { public final Map map; public Obj() { map = new HashMap(); } public Obj(Map m) { map = m; } public Object get(String k) { return map.get(k); } public boolean has(String k) { return map.get(k) != null; } public String optString(String k) { return optString(k, ""); } public String optString(String k, String def) { Object v = map.get(k); if (v == null) return def; if (v instanceof Double) { double d = ((Double) v).doubleValue(); if (d == Math.floor(d) && Math.abs(d) < 1e15) return Long.toString((long) d); } return v.toString(); } public long optLong(String k, long def) { Object v = map.get(k); if (v instanceof Number) return ((Number) v).longValue(); if (v instanceof String) { try { return (long) Double.parseDouble((String) v); } catch (NumberFormatException e) { return def; } } if (v instanceof Boolean) return ((Boolean) v).booleanValue() ? 1 : 0; return def; } public int optInt(String k, int def) { return (int) optLong(k, def); } public double optDouble(String k, double def) { Object v = map.get(k); if (v instanceof Number) return ((Number) v).doubleValue(); if (v instanceof String) { try { return Double.parseDouble((String) v); } catch (NumberFormatException e) { return def; } } return def; } public boolean optBool(String k) { Object v = map.get(k); if (v instanceof Boolean) return ((Boolean) v).booleanValue(); if (v instanceof Number) return ((Number) v).intValue() != 0; if (v instanceof String) return "1".equals(v) || "true".equals(v); return false; } public Obj optObj(String k) { Object v = map.get(k); return v instanceof Obj ? (Obj) v : null; } public Arr optArr(String k) { Object v = map.get(k); return v instanceof Arr ? (Arr) v : null; } public Obj put(String k, Object v) { map.put(k, v); return this; } @Override public String toString() { StringBuilder sb = new StringBuilder(); write(sb, this); return sb.toString(); } } public static final class Arr { public final List list; public Arr() { list = new ArrayList(); } public int size() { return list.size(); } public Object get(int i) { return list.get(i); } public Obj optObj(int i) { Object v = i < list.size() ? list.get(i) : null; return v instanceof Obj ? (Obj) v : null; } public Arr add(Object v) { list.add(v); return this; } @Override public String toString() { StringBuilder sb = new StringBuilder(); write(sb, this); return sb.toString(); } } // ------------------------------------------------------------------ parsing public static Object parse(String s) { P p = new P(s); p.ws(); Object v = p.value(); p.ws(); if (p.i < s.length()) throw new IllegalArgumentException("Trailing data at " + p.i); return v; } public static Obj parseObj(String s) { Object o = parse(s); if (!(o instanceof Obj)) throw new IllegalArgumentException("Expected JSON object"); return (Obj) o; } private static final class P { final String s; int i; P(String s) { this.s = s; } void ws() { while (i < s.length()) { char c = s.charAt(i); if (c == ' ' || c == '\n' || c == '\r' || c == '\t') i++; else break; } } IllegalArgumentException err(String m) { return new IllegalArgumentException(m + " at " + i); } Object value() { if (i >= s.length()) throw err("Unexpected end"); char c = s.charAt(i); switch (c) { case '{': return obj(); case '[': return arr(); case '"': return str(); case 't': expect("true"); return Boolean.TRUE; case 'f': expect("false"); return Boolean.FALSE; case 'n': expect("null"); return null; default: return num(); } } void expect(String w) { if (!s.startsWith(w, i)) throw err("Expected " + w); i += w.length(); } Obj obj() { Obj o = new Obj(); i++; ws(); if (i < s.length() && s.charAt(i) == '}') { i++; return o; } while (true) { ws(); if (i >= s.length() || s.charAt(i) != '"') throw err("Expected key"); String k = str(); ws(); if (i >= s.length() || s.charAt(i) != ':') throw err("Expected :"); i++; ws(); o.map.put(k, value()); ws(); if (i >= s.length()) throw err("Unterminated object"); char c = s.charAt(i++); if (c == '}') return o; if (c != ',') throw err("Expected , or }"); } } Arr arr() { Arr a = new Arr(); i++; ws(); if (i < s.length() && s.charAt(i) == ']') { i++; return a; } while (true) { ws(); a.list.add(value()); ws(); if (i >= s.length()) throw err("Unterminated array"); char c = s.charAt(i++); if (c == ']') return a; if (c != ',') throw err("Expected , or ]"); } } String str() { i++; // opening quote StringBuilder sb = null; int start = i; while (i < s.length()) { char c = s.charAt(i); if (c == '"') { String r = sb == null ? s.substring(start, i) : sb.append(s, start, i).toString(); i++; return r; } if (c == '\\') { if (sb == null) sb = new StringBuilder(); sb.append(s, start, i); i++; if (i >= s.length()) break; char e = s.charAt(i++); switch (e) { case 'n': sb.append('\n'); break; case 't': sb.append('\t'); break; case 'r': sb.append('\r'); break; case 'b': sb.append('\b'); break; case 'f': sb.append('\f'); break; case 'u': if (i + 4 > s.length()) throw err("Bad escape"); sb.append((char) Integer.parseInt(s.substring(i, i + 4), 16)); i += 4; break; default: sb.append(e); } start = i; } else { i++; } } throw err("Unterminated string"); } Object num() { int start = i; while (i < s.length()) { char c = s.charAt(i); if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E') i++; else break; } if (start == i) throw err("Unexpected character"); String n = s.substring(start, i); try { if (n.indexOf('.') < 0 && n.indexOf('e') < 0 && n.indexOf('E') < 0 && n.length() < 18) { return Long.valueOf(n); } return Double.valueOf(n); } catch (NumberFormatException e) { throw err("Bad number"); } } } // ------------------------------------------------------------------ writing public static void write(StringBuilder sb, Object v) { if (v == null) { sb.append("null"); return; } if (v instanceof String) { quote(sb, (String) v); return; } if (v instanceof Number || v instanceof Boolean) { sb.append(v.toString()); return; } if (v instanceof Obj) { sb.append('{'); boolean first = true; for (Iterator> it = ((Obj) v).map.entrySet().iterator(); it.hasNext();) { Map.Entry e = it.next(); if (e.getValue() == null) continue; if (!first) sb.append(','); first = false; quote(sb, e.getKey()); sb.append(':'); write(sb, e.getValue()); } sb.append('}'); return; } if (v instanceof Arr) { sb.append('['); List l = ((Arr) v).list; for (int i = 0; i < l.size(); i++) { if (i > 0) sb.append(','); write(sb, l.get(i)); } sb.append(']'); return; } quote(sb, v.toString()); } private static void quote(StringBuilder sb, String s) { sb.append('"'); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); switch (c) { case '"': sb.append("\\\""); break; case '\\': sb.append("\\\\"); break; case '\n': sb.append("\\n"); break; case '\r': sb.append("\\r"); break; case '\t': sb.append("\\t"); break; default: if (c < 0x20) { String h = Integer.toHexString(c); sb.append("\\u"); for (int k = h.length(); k < 4; k++) sb.append('0'); sb.append(h); } else sb.append(c); } } sb.append('"'); } }