Files
plexamp-lite/src/com/minamp/util/Json.java
T
2026-10-14 19:38:37 +02:00

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('"');
}
}