001package gudusoft.gsqlparser.pp.score.json;
002
003import java.util.ArrayList;
004import java.util.LinkedHashMap;
005import java.util.List;
006import java.util.Map;
007
008/**
009 * Minimal, dependency-free JSON reader (RFC 8259 subset: objects, arrays,
010 * strings, numbers, booleans, null; no comments). Objects become
011 * {@link LinkedHashMap}, arrays {@link ArrayList}, numbers {@link Double}
012 * when they carry a fraction/exponent and {@link Long} otherwise.
013 *
014 * <p>Used to load {@code ScoreProfile} resources and the CLI's option /
015 * run files. Package-private by design.
016 */
017public final class JsonIn {
018
019    private final String s;
020    private int i;
021
022    private JsonIn(String s) {
023        this.s = s;
024    }
025
026    public static Object parse(String text) {
027        if (text == null) throw new NullPointerException("text");
028        JsonIn p = new JsonIn(text);
029        p.ws();
030        Object v = p.value();
031        p.ws();
032        if (p.i != p.s.length()) {
033            throw p.err("trailing characters");
034        }
035        return v;
036    }
037
038    @SuppressWarnings("unchecked")
039    public static Map<String, Object> parseObject(String text) {
040        Object v = parse(text);
041        if (!(v instanceof Map)) throw new IllegalArgumentException("JSON root is not an object");
042        return (Map<String, Object>) v;
043    }
044
045    private Object value() {
046        if (i >= s.length()) throw err("unexpected end");
047        char c = s.charAt(i);
048        switch (c) {
049            case '{': return object();
050            case '[': return array();
051            case '"': return string();
052            case 't': expect("true"); return Boolean.TRUE;
053            case 'f': expect("false"); return Boolean.FALSE;
054            case 'n': expect("null"); return null;
055            default:
056                if (c == '-' || (c >= '0' && c <= '9')) return number();
057                throw err("unexpected character '" + c + "'");
058        }
059    }
060
061    private Map<String, Object> object() {
062        Map<String, Object> m = new LinkedHashMap<String, Object>();
063        i++; // {
064        ws();
065        if (peek() == '}') { i++; return m; }
066        while (true) {
067            ws();
068            if (peek() != '"') throw err("expected string key");
069            String k = string();
070            ws();
071            if (peek() != ':') throw err("expected ':'");
072            i++;
073            ws();
074            m.put(k, value());
075            ws();
076            char c = peek();
077            if (c == ',') { i++; continue; }
078            if (c == '}') { i++; return m; }
079            throw err("expected ',' or '}'");
080        }
081    }
082
083    private List<Object> array() {
084        List<Object> a = new ArrayList<Object>();
085        i++; // [
086        ws();
087        if (peek() == ']') { i++; return a; }
088        while (true) {
089            ws();
090            a.add(value());
091            ws();
092            char c = peek();
093            if (c == ',') { i++; continue; }
094            if (c == ']') { i++; return a; }
095            throw err("expected ',' or ']'");
096        }
097    }
098
099    private String string() {
100        StringBuilder sb = new StringBuilder();
101        i++; // opening quote
102        while (i < s.length()) {
103            char c = s.charAt(i++);
104            if (c == '"') return sb.toString();
105            if (c == '\\') {
106                if (i >= s.length()) throw err("bad escape");
107                char e = s.charAt(i++);
108                switch (e) {
109                    case '"': sb.append('"'); break;
110                    case '\\': sb.append('\\'); break;
111                    case '/': sb.append('/'); break;
112                    case 'b': sb.append('\b'); break;
113                    case 'f': sb.append('\f'); break;
114                    case 'n': sb.append('\n'); break;
115                    case 'r': sb.append('\r'); break;
116                    case 't': sb.append('\t'); break;
117                    case 'u':
118                        if (i + 4 > s.length()) throw err("bad \\u escape");
119                        sb.append((char) Integer.parseInt(s.substring(i, i + 4), 16));
120                        i += 4;
121                        break;
122                    default: throw err("bad escape \\" + e);
123                }
124            } else {
125                sb.append(c);
126            }
127        }
128        throw err("unterminated string");
129    }
130
131    private Number number() {
132        int start = i;
133        boolean frac = false;
134        if (peek() == '-') i++;
135        while (i < s.length()) {
136            char c = s.charAt(i);
137            if (c >= '0' && c <= '9') { i++; }
138            else if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') { frac = true; i++; }
139            else break;
140        }
141        String t = s.substring(start, i);
142        try {
143            if (frac) return Double.valueOf(t);
144            return Long.valueOf(t);
145        } catch (NumberFormatException ex) {
146            throw err("bad number " + t);
147        }
148    }
149
150    private void expect(String lit) {
151        if (!s.startsWith(lit, i)) throw err("expected " + lit);
152        i += lit.length();
153    }
154
155    private void ws() {
156        while (i < s.length()) {
157            char c = s.charAt(i);
158            if (c == ' ' || c == '\t' || c == '\n' || c == '\r') i++;
159            else break;
160        }
161    }
162
163    private char peek() {
164        if (i >= s.length()) throw err("unexpected end");
165        return s.charAt(i);
166    }
167
168    private IllegalArgumentException err(String msg) {
169        return new IllegalArgumentException("JSON parse error at " + i + ": " + msg);
170    }
171
172    // ---- typed helpers for callers -------------------------------------
173
174    public static String str(Map<String, Object> m, String key, String dflt) {
175        Object v = m.get(key);
176        return v == null ? dflt : v.toString();
177    }
178
179    public static double dbl(Map<String, Object> m, String key, double dflt) {
180        Object v = m.get(key);
181        return v instanceof Number ? ((Number) v).doubleValue() : dflt;
182    }
183
184    public static int integer(Map<String, Object> m, String key, int dflt) {
185        Object v = m.get(key);
186        return v instanceof Number ? ((Number) v).intValue() : dflt;
187    }
188
189    public static boolean bool(Map<String, Object> m, String key, boolean dflt) {
190        Object v = m.get(key);
191        return v instanceof Boolean ? ((Boolean) v).booleanValue() : dflt;
192    }
193
194    @SuppressWarnings("unchecked")
195    public static Map<String, Object> obj(Map<String, Object> m, String key) {
196        Object v = m.get(key);
197        return v instanceof Map ? (Map<String, Object>) v : new LinkedHashMap<String, Object>();
198    }
199
200    @SuppressWarnings("unchecked")
201    public static List<Object> arr(Map<String, Object> m, String key) {
202        Object v = m.get(key);
203        return v instanceof List ? (List<Object>) v : new ArrayList<Object>();
204    }
205}