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}