package com.miniabs; import java.io.IOException; import java.io.Reader; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * Small streaming JSON parser that builds org.json objects straight from the * socket. Very long strings (episode show notes) are cut short and bulky keys * are dropped while reading, so a podcast with hundreds of episodes still fits * in the Galaxy Mini's small heap. */ final class Json { /** Keys never kept, at any depth. */ private static final String[] ALWAYS_SKIP = {"chapters", "libraryFiles", "metaTags", "rssFeed"}; private final Reader in; private final char[] buf = new char[8192]; private int pos, len; private final int maxString; private final String[] deepSkip; private int depth; private Json(Reader in, int maxString, String[] deepSkip) { this.in = in; this.maxString = maxString; this.deepSkip = deepSkip; } static Object parse(Reader r) throws IOException { return parse(r, 2000, null); } /** * @param maxString longest string kept (longer ones end with an ellipsis) * @param deepSkip keys dropped inside objects nested three or more levels deep */ static Object parse(Reader r, int maxString, String[] deepSkip) throws IOException { Json j = new Json(r, maxString, deepSkip); int c = j.skipWs(); if (c == -1) return null; return j.value(c); } private int read() throws IOException { if (pos >= len) { len = in.read(buf, 0, buf.length); pos = 0; if (len <= 0) { len = 0; return -1; } } return buf[pos++]; } private void unread() { pos--; } private int skipWs() throws IOException { int c; do { c = read(); } while (c == ' ' || c == '\n' || c == '\r' || c == '\t'); return c; } private Object value(int c) throws IOException { switch (c) { case '{': return object(); case '[': return array(); case '"': return string(maxString); case 't': expect("rue"); return Boolean.TRUE; case 'f': expect("alse"); return Boolean.FALSE; case 'n': expect("ull"); return JSONObject.NULL; default: if (c == '-' || (c >= '0' && c <= '9')) return number(c); throw new IOException("Resposta inesperada do servidor"); } } private void expect(String rest) throws IOException { for (int i = 0; i < rest.length(); i++) { if (read() != rest.charAt(i)) throw new IOException("Resposta inesperada do servidor"); } } private Object number(int first) throws IOException { StringBuilder sb = new StringBuilder(16); sb.append((char) first); boolean dbl = false; int c; while (true) { c = read(); if ((c >= '0' && c <= '9') || c == '-' || c == '+') { sb.append((char) c); } else if (c == '.' || c == 'e' || c == 'E') { dbl = true; sb.append((char) c); } else { break; } } if (c != -1) unread(); String s = sb.toString(); try { if (!dbl) { long l = Long.parseLong(s); if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) return Integer.valueOf((int) l); return Long.valueOf(l); } return Double.valueOf(s); } catch (NumberFormatException e) { try { return Double.valueOf(s); } catch (NumberFormatException e2) { return Integer.valueOf(0); } } } private boolean skipKey(String key) { for (String k : ALWAYS_SKIP) if (k.equals(key)) return true; if (deepSkip != null && depth >= 3) { for (String k : deepSkip) if (k.equals(key)) return true; } return false; } private JSONObject object() throws IOException { depth++; JSONObject o = new JSONObject(); int c = skipWs(); if (c == '}') { depth--; return o; } while (true) { if (c != '"') throw new IOException("Resposta inesperada do servidor"); String key = string(512); c = skipWs(); if (c != ':') throw new IOException("Resposta inesperada do servidor"); c = skipWs(); if (skipKey(key)) { skipValue(c); } else { Object v = value(c); try { o.put(key, v); } catch (JSONException ignored) { // NaN or similar; drop it } } c = skipWs(); if (c == ',') { c = skipWs(); continue; } if (c == '}') { depth--; return o; } throw new IOException("Resposta inesperada do servidor"); } } private JSONArray array() throws IOException { JSONArray a = new JSONArray(); int c = skipWs(); if (c == ']') return a; while (true) { a.put(value(c)); c = skipWs(); if (c == ',') { c = skipWs(); continue; } if (c == ']') return a; throw new IOException("Resposta inesperada do servidor"); } } private void skipValue(int c) throws IOException { if (c == '"') { string(0); return; } if (c == '{' || c == '[') { int d = 1; while (d > 0) { int ch = read(); if (ch == -1) throw new IOException("A resposta chegou incompleta"); if (ch == '"') string(0); else if (ch == '{' || ch == '[') d++; else if (ch == '}' || ch == ']') d--; } return; } value(c); } private String string(int max) throws IOException { StringBuilder sb = new StringBuilder(max > 0 ? Math.min(max, 32) : 0); boolean cut = false; while (true) { int c = read(); if (c == -1) throw new IOException("A resposta chegou incompleta"); if (c == '"') break; if (c == '\\') { c = read(); switch (c) { case 'n': c = '\n'; break; case 't': c = '\t'; break; case 'r': c = '\r'; break; case 'b': c = '\b'; break; case 'f': c = '\f'; break; case 'u': { int v = 0; for (int i = 0; i < 4; i++) { int d = Character.digit(read(), 16); if (d < 0) throw new IOException("Resposta inesperada do servidor"); v = v * 16 + d; } c = v; break; } case -1: throw new IOException("A resposta chegou incompleta"); default: break; // \" \\ \/ } } if (sb.length() < max) sb.append((char) c); else cut = true; } if (cut && max > 0) sb.append('…'); return sb.toString(); } }