ce580e03601410dceb1f83cacf277dcfc5045683
[jalview.git] / src / javajs / util / JSJSONParser.java
1 package javajs.util;
2
3 import java.util.HashMap;
4 import java.util.Hashtable;
5 import java.util.Map;
6
7 import javajs.J2SIgnoreImport;
8
9
10
11 /**
12  * a very simple JSON parser for JSON objects that are compatible with JavaScript
13  * A gross simplification of https://github.com/douglascrockford/JSON-java
14  * 
15  * A SUBSET of JSON with similarly to window.JSON.parse():
16  * 
17  * In JavaScript returns "null" for a null value, not null
18  * 
19  *  -- requires quoted strings for keys and values
20  *  
21  *  -- does not allow /xxx/ objects
22  *  
23  *  @author Bob Hanson
24  *  
25  */
26 @J2SIgnoreImport({ HashMap.class })
27 public class JSJSONParser {
28
29   private String str;
30   private int index;
31   private int len;
32   private boolean asHashTable;
33
34   public JSJSONParser () {
35     // for reflection
36   }
37   
38   /**
39    * requires { "key":"value", "key":"value",....}
40    * 
41    * @param str
42    * @param asHashTable TODO
43    * 
44    * @return Map or null
45    */
46   @SuppressWarnings("unchecked")
47   public Map<String, Object> parseMap(String str, boolean asHashTable) {
48     index = 0;
49     this.asHashTable = asHashTable;
50     this.str = str;
51     len = str.length();
52     if (getChar() != '{')
53       return null;
54     returnChar();
55     return (Map<String, Object>) getValue(false);
56   }
57   
58   /**
59    * Could return Integer, Float, Boolean, String, Map<String, Object>, Lst<Object>, or null
60    * 
61    * @param str
62    * @return a object equivalent to the JSON string str
63    * 
64    */
65   public Object parse(String str) {
66     index = 0;
67     this.str = str;
68     len = str.length();
69     return getValue(false);
70   }
71
72   private char next() {
73     return (index < len ? str.charAt(index++) : '\0');
74   }
75
76   private void returnChar() {
77     index--;
78   }
79
80   /**
81    * Get the next char in the string, skipping whitespace.
82    * 
83    * @throws JSONException
84    * @return one character, or 0 if there are no more characters.
85    */
86   private char getChar() throws JSONException {
87     for (;;) {
88       char c = next();
89       if (c == 0 || c > ' ') {
90         return c;
91       }
92     }
93   }
94
95   /**
96    * only allowing the following values:
97    * 
98    * {...} object
99    * 
100    * [...] array
101    * 
102    * Integer
103    * 
104    * Float
105    * 
106    * "quoted string"
107    * 
108    * 
109    * @param isKey if we should allow {...} and [...]
110    * @return a subclass of Object
111    * @throws JSONException
112    */
113   private Object getValue(boolean isKey) throws JSONException {
114     int i = index;
115     char c = getChar();
116     switch (c) {
117     case '\0':
118       return null;
119     case '"':
120     case '\'':
121       return getString(c);
122     case '{':
123       if (!isKey)
124         return getObject();
125       c = 0;
126       break;
127     case '[':
128       if (!isKey)
129         return getArray();
130       c = 0;
131       break;
132     default:
133       // standard syntax is assumed; not checking all possible invalid keys
134       // for example, "-" is not allowed in JavaScript, which is what this is for
135       returnChar();
136       while (c >= ' ' && "[,]{:}'\"".indexOf(c) < 0)
137         c = next();
138       returnChar();
139       if (isKey && c != ':')
140         c = 0;
141       break;
142     }
143     if (isKey && c == 0)
144       throw new JSONException("invalid key");
145
146     String string = str.substring(i, index);
147
148     // check for the only valid simple words: true, false, null (lower case)
149     // and in this case, only for 
150
151     if (!isKey) {
152       if (string.equals("true")) {
153         return Boolean.TRUE;
154       }
155       if (string.equals("false")) {
156         return Boolean.FALSE;
157       }
158       if (string.equals("null")) {
159         return (asHashTable ? string : null);
160       }
161     }
162     //  only numbers from here on:
163
164     c = string.charAt(0);
165     if (c >= '0' && c <= '9' || c == '-')
166       try {
167         if (string.indexOf('.') < 0 && string.indexOf('e') < 0
168             && string.indexOf('E') < 0)
169           return new Integer(string);
170         // not allowing infinity or NaN
171         // using float here because Jmol does not use Double
172         Float d = Float.valueOf(string);
173         if (!d.isInfinite() && !d.isNaN())
174           return d;
175       } catch (Exception e) {
176       }
177     // not a valid number
178     System.out.println("JSON parser cannot parse " + string);
179     throw new JSONException("invalid value");
180   }
181
182   private String getString(char quote) throws JSONException {
183     char c;
184     SB sb = null;
185     int i0 = index;
186     for (;;) {
187       int i1 = index;
188       switch (c = next()) {
189       case '\0':
190       case '\n':
191       case '\r':
192         throw syntaxError("Unterminated string");
193       case '\\':
194         switch (c = next()) {
195         case '"':
196         case '\'':
197         case '\\':
198         case '/':
199           break;
200         case 'b':
201           c = '\b';
202           break;
203         case 't':
204           c = '\t';
205           break;
206         case 'n':
207           c = '\n';
208           break;
209         case 'f':
210           c = '\f';
211           break;
212         case 'r':
213           c = '\r';
214           break;
215         case 'u':
216           int i = index;
217           index += 4;
218           try {
219             c = (char) Integer.parseInt(str.substring(i, index), 16);
220           } catch (Exception e) {
221             throw syntaxError("Substring bounds error");
222           }
223           break;
224         default:
225           throw syntaxError("Illegal escape.");
226         }
227         break;
228       default:
229         if (c == quote)
230           return (sb == null ? str.substring(i0, i1) : sb.toString());
231         break;
232       }
233       if (index > i1 + 1) {
234         if (sb == null) {
235           sb = new SB();
236           sb.append(str.substring(i0, i1));
237         }
238       }
239       if (sb != null)
240         sb.appendC(c);
241     }
242   }
243
244   private Object getObject() {
245     Map<String, Object> map = (asHashTable ? new Hashtable<String, Object>() : new HashMap<String, Object>());
246     String key = null;
247     switch (getChar()) {
248     case '}':
249       return map;
250     case 0:
251       throw new JSONException("invalid object");
252     }
253     returnChar();
254     boolean isKey = false;
255     for (;;) {
256       if ((isKey = !isKey) == true)
257         key = getValue(true).toString();
258       else
259         map.put(key, getValue(false));
260       switch (getChar()) {
261       case '}':
262         return map;
263       case ':':
264         if (isKey)
265           continue;
266         isKey = true;
267         //$FALL-THROUGH$
268       case ',':
269         if (!isKey)
270           continue;
271         //$FALL-THROUGH$
272       default:
273         throw syntaxError("Expected ',' or ':' or '}'");
274       }
275     }
276   }
277
278   private Object getArray() {
279     Lst<Object> l = new Lst<Object>();
280     switch (getChar()) {
281     case ']':
282       return l;
283     case 0:
284       throw new JSONException("invalid array");
285     }
286     returnChar();
287     boolean isNull = false;
288     for (;;) {
289       if (isNull) {
290         l.addLast(null);
291         isNull = false;
292       } else {
293         l.addLast(getValue(false));
294       }
295       switch (getChar()) {
296       case ',':
297         switch (getChar()) {
298         case ']':
299           // terminal ,
300           return l;
301         case ',':
302           // empty value
303           isNull = true;
304           //$FALL-THROUGH$
305         default:
306           returnChar();
307         }
308         continue;
309       case ']':
310         return l;
311       default:
312         throw syntaxError("Expected ',' or ']'");
313       }
314     }
315   }
316
317   /**
318    * Make a JSONException to signal a syntax error.
319    * 
320    * @param message
321    *        The error message.
322    * @return A JSONException object, suitable for throwing
323    */
324   public JSONException syntaxError(String message) {
325     return new JSONException(message + " for " + str.substring(0, Math.min(index,  len)));
326   }
327
328 }