package javajs.util; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; /** * a very simple JSON parser for JSON objects that are compatible with JavaScript * A gross simplification of https://github.com/douglascrockford/JSON-java * * A SUBSET of JSON with similarly to window.JSON.parse(): * * In JavaScript returns "null" for a null value, not null * * -- requires quoted strings for keys and values * * -- does not allow /xxx/ objects * * @author Bob Hanson * */ public class JSJSONParser { private String str; private int index; private int len; private boolean asHashTable; public JSJSONParser () { // for reflection } /** * requires { "key":"value", "key":"value",....} * * @param str * @param asHashTable TODO * * @return Map or null */ @SuppressWarnings("unchecked") public Map parseMap(String str, boolean asHashTable) { index = 0; this.asHashTable = asHashTable; this.str = str; len = str.length(); if (getChar() != '{') return null; returnChar(); return (Map) getValue(false); } /** * Could return Integer, Float, Boolean, String, Map, Lst, or null * * @param str * @param asHashTable * @return a object equivalent to the JSON string str * */ public Object parse(String str, boolean asHashTable) { index = 0; this.asHashTable = asHashTable; this.str = str; len = str.length(); return getValue(false); } private char next() { return (index < len ? str.charAt(index++) : '\0'); } private void returnChar() { index--; } /** * Get the next char in the string, skipping whitespace. * * @throws JSONException * @return one character, or 0 if there are no more characters. */ private char getChar() throws JSONException { for (;;) { char c = next(); if (c == 0 || c > ' ') { return c; } } } /** * only allowing the following values: * * {...} object * * [...] array * * Integer * * Float * * "quoted string" * * * @param isKey if we should allow {...} and [...] * @return a subclass of Object * @throws JSONException */ private Object getValue(boolean isKey) throws JSONException { int i = index; char c = getChar(); switch (c) { case '\0': return null; case '"': case '\'': return getString(c); case '{': if (!isKey) return getObject(); c = 0; break; case '[': if (!isKey) return getArray(); c = 0; break; default: // standard syntax is assumed; not checking all possible invalid keys // for example, "-" is not allowed in JavaScript, which is what this is for returnChar(); while (c >= ' ' && "[,]{:}'\"".indexOf(c) < 0) c = next(); returnChar(); if (isKey && c != ':') c = 0; break; } if (isKey && c == 0) throw new JSONException("invalid key"); String string = str.substring(i, index).trim(); // check for the only valid simple words: true, false, null (lower case) // and in this case, only for if (!isKey) { if (string.equals("true")) { return Boolean.TRUE; } if (string.equals("false")) { return Boolean.FALSE; } if (string.equals("null")) { return (asHashTable ? string : null); } } // only numbers from here on: c = string.charAt(0); if (c >= '0' && c <= '9' || c == '-') try { if (string.indexOf('.') < 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0) return new Integer(string); // not allowing infinity or NaN // using float here because Jmol does not use Double Float d = Float.valueOf(string); if (!d.isInfinite() && !d.isNaN()) return d; } catch (Exception e) { } // not a valid number System.out.println("JSON parser cannot parse " + string); throw new JSONException("invalid value"); } private String getString(char quote) throws JSONException { char c; SB sb = null; int i0 = index; for (;;) { int i1 = index; switch (c = next()) { case '\0': case '\n': case '\r': throw syntaxError("Unterminated string"); case '\\': switch (c = next()) { case '"': case '\'': case '\\': case '/': break; case 'b': c = '\b'; break; case 't': c = '\t'; break; case 'n': c = '\n'; break; case 'f': c = '\f'; break; case 'r': c = '\r'; break; case 'u': int i = index; index += 4; try { c = (char) Integer.parseInt(str.substring(i, index), 16); } catch (Exception e) { throw syntaxError("Substring bounds error"); } break; default: throw syntaxError("Illegal escape."); } break; default: if (c == quote) return (sb == null ? str.substring(i0, i1) : sb.toString()); break; } if (index > i1 + 1) { if (sb == null) { sb = new SB(); sb.append(str.substring(i0, i1)); } } if (sb != null) sb.appendC(c); } } private Object getObject() { Map map = (asHashTable ? new Hashtable() : new HashMap()); String key = null; switch (getChar()) { case '}': return map; case 0: throw new JSONException("invalid object"); } returnChar(); boolean isKey = false; for (;;) { if ((isKey = !isKey) == true) key = getValue(true).toString(); else map.put(key, getValue(false)); switch (getChar()) { case '}': return map; case ':': if (isKey) continue; isKey = true; //$FALL-THROUGH$ case ',': if (!isKey) continue; //$FALL-THROUGH$ default: throw syntaxError("Expected ',' or ':' or '}'"); } } } private Object getArray() { Lst l = new Lst(); switch (getChar()) { case ']': return l; case '\0': throw new JSONException("invalid array"); } returnChar(); boolean isNull = false; for (;;) { if (isNull) { l.addLast(null); isNull = false; } else { l.addLast(getValue(false)); } switch (getChar()) { case ',': switch (getChar()) { case ']': // terminal , return l; case ',': // empty value isNull = true; //$FALL-THROUGH$ default: returnChar(); } continue; case ']': return l; default: throw syntaxError("Expected ',' or ']'"); } } } /** * Make a JSONException to signal a syntax error. * * @param message * The error message. * @return A JSONException object, suitable for throwing */ public JSONException syntaxError(String message) { return new JSONException(message + " for " + str.substring(0, Math.min(index, len))); } }