Private
Public Access
284 lines
10 KiB
Java
284 lines
10 KiB
Java
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<String, Object> map;
|
|
public Obj() { map = new HashMap<String, Object>(); }
|
|
public Obj(Map<String, Object> 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<Object> list;
|
|
public Arr() { list = new ArrayList<Object>(); }
|
|
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<Map.Entry<String, Object>> it = ((Obj) v).map.entrySet().iterator(); it.hasNext();) {
|
|
Map.Entry<String, Object> 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<Object> 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('"');
|
|
}
|
|
}
|