|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.sync; |
|
6 |
|
7 import java.io.IOException; |
|
8 import java.io.Reader; |
|
9 import java.io.StringReader; |
|
10 import java.util.Map; |
|
11 import java.util.Map.Entry; |
|
12 import java.util.Set; |
|
13 |
|
14 import org.json.simple.JSONArray; |
|
15 import org.json.simple.JSONObject; |
|
16 import org.json.simple.parser.JSONParser; |
|
17 import org.json.simple.parser.ParseException; |
|
18 import org.mozilla.apache.commons.codec.binary.Base64; |
|
19 import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; |
|
20 |
|
21 /** |
|
22 * Extend JSONObject to do little things, like, y'know, accessing members. |
|
23 * |
|
24 * @author rnewman |
|
25 * |
|
26 */ |
|
27 public class ExtendedJSONObject { |
|
28 |
|
29 public JSONObject object; |
|
30 |
|
31 /** |
|
32 * Return a <code>JSONParser</code> instance for immediate use. |
|
33 * <p> |
|
34 * <code>JSONParser</code> is not thread-safe, so we return a new instance |
|
35 * each call. This is extremely inefficient in execution time and especially |
|
36 * memory use -- each instance allocates a 16kb temporary buffer -- and we |
|
37 * hope to improve matters eventually. |
|
38 */ |
|
39 protected static JSONParser getJSONParser() { |
|
40 return new JSONParser(); |
|
41 } |
|
42 |
|
43 /** |
|
44 * Parse a JSON encoded string. |
|
45 * |
|
46 * @param in <code>Reader</code> over a JSON-encoded input to parse; not |
|
47 * necessarily a JSON object. |
|
48 * @return a regular Java <code>Object</code>. |
|
49 * @throws ParseException |
|
50 * @throws IOException |
|
51 */ |
|
52 protected static Object parseRaw(Reader in) throws ParseException, IOException { |
|
53 try { |
|
54 return getJSONParser().parse(in); |
|
55 } catch (Error e) { |
|
56 // Don't be stupid, org.json.simple. Bug 1042929. |
|
57 throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION); |
|
58 } |
|
59 } |
|
60 |
|
61 /** |
|
62 * Parse a JSON encoded string. |
|
63 * <p> |
|
64 * You should prefer the streaming interface {@link #parseRaw(Reader)}. |
|
65 * |
|
66 * @param input JSON-encoded input string to parse; not necessarily a JSON object. |
|
67 * @return a regular Java <code>Object</code>. |
|
68 * @throws ParseException |
|
69 */ |
|
70 protected static Object parseRaw(String input) throws ParseException { |
|
71 try { |
|
72 return getJSONParser().parse(input); |
|
73 } catch (Error e) { |
|
74 // Don't be stupid, org.json.simple. Bug 1042929. |
|
75 throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION); |
|
76 } |
|
77 } |
|
78 |
|
79 /** |
|
80 * Helper method to get a JSON array from a stream. |
|
81 * |
|
82 * @param in <code>Reader</code> over a JSON-encoded array to parse. |
|
83 * @throws ParseException |
|
84 * @throws IOException |
|
85 * @throws NonArrayJSONException if the object is valid JSON, but not an array. |
|
86 */ |
|
87 public static JSONArray parseJSONArray(Reader in) |
|
88 throws IOException, ParseException, NonArrayJSONException { |
|
89 Object o = parseRaw(in); |
|
90 |
|
91 if (o == null) { |
|
92 return null; |
|
93 } |
|
94 |
|
95 if (o instanceof JSONArray) { |
|
96 return (JSONArray) o; |
|
97 } |
|
98 |
|
99 throw new NonArrayJSONException("value must be a JSON array"); |
|
100 } |
|
101 |
|
102 /** |
|
103 * Helper method to get a JSON array from a string. |
|
104 * <p> |
|
105 * You should prefer the stream interface {@link #parseJSONArray(Reader)}. |
|
106 * |
|
107 * @param jsonString input. |
|
108 * @throws ParseException |
|
109 * @throws IOException |
|
110 * @throws NonArrayJSONException if the object is valid JSON, but not an array. |
|
111 */ |
|
112 public static JSONArray parseJSONArray(String jsonString) |
|
113 throws IOException, ParseException, NonArrayJSONException { |
|
114 Object o = parseRaw(jsonString); |
|
115 |
|
116 if (o == null) { |
|
117 return null; |
|
118 } |
|
119 |
|
120 if (o instanceof JSONArray) { |
|
121 return (JSONArray) o; |
|
122 } |
|
123 |
|
124 throw new NonArrayJSONException("value must be a JSON array"); |
|
125 } |
|
126 |
|
127 /** |
|
128 * Helper method to get a JSON object from a stream. |
|
129 * |
|
130 * @param in input {@link Reader}. |
|
131 * @throws ParseException |
|
132 * @throws IOException |
|
133 * @throws NonArrayJSONException if the object is valid JSON, but not an object. |
|
134 */ |
|
135 public static ExtendedJSONObject parseJSONObject(Reader in) |
|
136 throws IOException, ParseException, NonObjectJSONException { |
|
137 return new ExtendedJSONObject(in); |
|
138 } |
|
139 |
|
140 /** |
|
141 * Helper method to get a JSON object from a string. |
|
142 * <p> |
|
143 * You should prefer the stream interface {@link #parseJSONObject(Reader)}. |
|
144 * |
|
145 * @param jsonString input. |
|
146 * @throws ParseException |
|
147 * @throws IOException |
|
148 * @throws NonObjectJSONException if the object is valid JSON, but not an object. |
|
149 */ |
|
150 public static ExtendedJSONObject parseJSONObject(String jsonString) |
|
151 throws IOException, ParseException, NonObjectJSONException { |
|
152 return new ExtendedJSONObject(jsonString); |
|
153 } |
|
154 |
|
155 /** |
|
156 * Helper method to get a JSON object from a UTF-8 byte array. |
|
157 * |
|
158 * @param in UTF-8 bytes. |
|
159 * @throws ParseException |
|
160 * @throws NonObjectJSONException if the object is valid JSON, but not an object. |
|
161 * @throws IOException |
|
162 */ |
|
163 public static ExtendedJSONObject parseUTF8AsJSONObject(byte[] in) |
|
164 throws ParseException, NonObjectJSONException, IOException { |
|
165 return parseJSONObject(new String(in, "UTF-8")); |
|
166 } |
|
167 |
|
168 public ExtendedJSONObject() { |
|
169 this.object = new JSONObject(); |
|
170 } |
|
171 |
|
172 public ExtendedJSONObject(JSONObject o) { |
|
173 this.object = o; |
|
174 } |
|
175 |
|
176 public ExtendedJSONObject(Reader in) throws IOException, ParseException, NonObjectJSONException { |
|
177 if (in == null) { |
|
178 this.object = new JSONObject(); |
|
179 return; |
|
180 } |
|
181 |
|
182 Object obj = parseRaw(in); |
|
183 if (obj instanceof JSONObject) { |
|
184 this.object = ((JSONObject) obj); |
|
185 } else { |
|
186 throw new NonObjectJSONException("value must be a JSON object"); |
|
187 } |
|
188 } |
|
189 |
|
190 public ExtendedJSONObject(String jsonString) throws IOException, ParseException, NonObjectJSONException { |
|
191 this(jsonString == null ? null : new StringReader(jsonString)); |
|
192 } |
|
193 |
|
194 // Passthrough methods. |
|
195 public Object get(String key) { |
|
196 return this.object.get(key); |
|
197 } |
|
198 |
|
199 public Long getLong(String key) { |
|
200 return (Long) this.get(key); |
|
201 } |
|
202 |
|
203 public String getString(String key) { |
|
204 return (String) this.get(key); |
|
205 } |
|
206 |
|
207 public Boolean getBoolean(String key) { |
|
208 return (Boolean) this.get(key); |
|
209 } |
|
210 |
|
211 /** |
|
212 * Return an Integer if the value for this key is an Integer, Long, or String |
|
213 * that can be parsed as a base 10 Integer. |
|
214 * Passes through null. |
|
215 * |
|
216 * @throws NumberFormatException |
|
217 */ |
|
218 public Integer getIntegerSafely(String key) throws NumberFormatException { |
|
219 Object val = this.object.get(key); |
|
220 if (val == null) { |
|
221 return null; |
|
222 } |
|
223 if (val instanceof Integer) { |
|
224 return (Integer) val; |
|
225 } |
|
226 if (val instanceof Long) { |
|
227 return Integer.valueOf(((Long) val).intValue()); |
|
228 } |
|
229 if (val instanceof String) { |
|
230 return Integer.parseInt((String) val, 10); |
|
231 } |
|
232 throw new NumberFormatException("Expecting Integer, got " + val.getClass()); |
|
233 } |
|
234 |
|
235 /** |
|
236 * Return a server timestamp value as milliseconds since epoch. |
|
237 * |
|
238 * @param key |
|
239 * @return A Long, or null if the value is non-numeric or doesn't exist. |
|
240 */ |
|
241 public Long getTimestamp(String key) { |
|
242 Object val = this.object.get(key); |
|
243 |
|
244 // This is absurd. |
|
245 if (val instanceof Double) { |
|
246 double millis = ((Double) val).doubleValue() * 1000; |
|
247 return Double.valueOf(millis).longValue(); |
|
248 } |
|
249 if (val instanceof Float) { |
|
250 double millis = ((Float) val).doubleValue() * 1000; |
|
251 return Double.valueOf(millis).longValue(); |
|
252 } |
|
253 if (val instanceof Number) { |
|
254 // Must be an integral number. |
|
255 return ((Number) val).longValue() * 1000; |
|
256 } |
|
257 |
|
258 return null; |
|
259 } |
|
260 |
|
261 public boolean containsKey(String key) { |
|
262 return this.object.containsKey(key); |
|
263 } |
|
264 |
|
265 public String toJSONString() { |
|
266 return this.object.toJSONString(); |
|
267 } |
|
268 |
|
269 public String toString() { |
|
270 return this.object.toString(); |
|
271 } |
|
272 |
|
273 public void put(String key, Object value) { |
|
274 @SuppressWarnings("unchecked") |
|
275 Map<Object, Object> map = this.object; |
|
276 map.put(key, value); |
|
277 } |
|
278 |
|
279 @SuppressWarnings({ "unchecked", "rawtypes" }) |
|
280 public void putAll(Map map) { |
|
281 this.object.putAll(map); |
|
282 } |
|
283 |
|
284 /** |
|
285 * Remove key-value pair from JSONObject. |
|
286 * |
|
287 * @param key |
|
288 * to be removed. |
|
289 * @return true if key exists and was removed, false otherwise. |
|
290 */ |
|
291 public boolean remove(String key) { |
|
292 Object res = this.object.remove(key); |
|
293 return (res != null); |
|
294 } |
|
295 |
|
296 public ExtendedJSONObject getObject(String key) throws NonObjectJSONException { |
|
297 Object o = this.object.get(key); |
|
298 if (o == null) { |
|
299 return null; |
|
300 } |
|
301 if (o instanceof ExtendedJSONObject) { |
|
302 return (ExtendedJSONObject) o; |
|
303 } |
|
304 if (o instanceof JSONObject) { |
|
305 return new ExtendedJSONObject((JSONObject) o); |
|
306 } |
|
307 throw new NonObjectJSONException("key must be a JSON object: " + key); |
|
308 } |
|
309 |
|
310 @SuppressWarnings("unchecked") |
|
311 public Set<Entry<String, Object>> entrySet() { |
|
312 return this.object.entrySet(); |
|
313 } |
|
314 |
|
315 @SuppressWarnings("unchecked") |
|
316 public Set<String> keySet() { |
|
317 return this.object.keySet(); |
|
318 } |
|
319 |
|
320 public org.json.simple.JSONArray getArray(String key) throws NonArrayJSONException { |
|
321 Object o = this.object.get(key); |
|
322 if (o == null) { |
|
323 return null; |
|
324 } |
|
325 if (o instanceof JSONArray) { |
|
326 return (JSONArray) o; |
|
327 } |
|
328 throw new NonArrayJSONException("key must be a JSON array: " + key); |
|
329 } |
|
330 |
|
331 public int size() { |
|
332 return this.object.size(); |
|
333 } |
|
334 |
|
335 @Override |
|
336 public int hashCode() { |
|
337 if (this.object == null) { |
|
338 return getClass().hashCode(); |
|
339 } |
|
340 return this.object.hashCode() ^ getClass().hashCode(); |
|
341 } |
|
342 |
|
343 @Override |
|
344 public boolean equals(Object o) { |
|
345 if (o == null || !(o instanceof ExtendedJSONObject)) { |
|
346 return false; |
|
347 } |
|
348 if (o == this) { |
|
349 return true; |
|
350 } |
|
351 ExtendedJSONObject other = (ExtendedJSONObject) o; |
|
352 if (this.object == null) { |
|
353 return other.object == null; |
|
354 } |
|
355 return this.object.equals(other.object); |
|
356 } |
|
357 |
|
358 /** |
|
359 * Throw if keys are missing or values have wrong types. |
|
360 * |
|
361 * @param requiredFields list of required keys. |
|
362 * @param requiredFieldClass class that values must be coercable to; may be null, which means don't check. |
|
363 * @throws UnexpectedJSONException |
|
364 */ |
|
365 public void throwIfFieldsMissingOrMisTyped(String[] requiredFields, Class<?> requiredFieldClass) throws BadRequiredFieldJSONException { |
|
366 // Defensive as possible: verify object has expected key(s) with string value. |
|
367 for (String k : requiredFields) { |
|
368 Object value = get(k); |
|
369 if (value == null) { |
|
370 throw new BadRequiredFieldJSONException("Expected key not present in result: " + k); |
|
371 } |
|
372 if (requiredFieldClass != null && !(requiredFieldClass.isInstance(value))) { |
|
373 throw new BadRequiredFieldJSONException("Value for key not an instance of " + requiredFieldClass + ": " + k); |
|
374 } |
|
375 } |
|
376 } |
|
377 |
|
378 /** |
|
379 * Return a base64-encoded string value as a byte array. |
|
380 */ |
|
381 public byte[] getByteArrayBase64(String key) { |
|
382 String s = (String) this.object.get(key); |
|
383 if (s == null) { |
|
384 return null; |
|
385 } |
|
386 return Base64.decodeBase64(s); |
|
387 } |
|
388 |
|
389 /** |
|
390 * Return a hex-encoded string value as a byte array. |
|
391 */ |
|
392 public byte[] getByteArrayHex(String key) { |
|
393 String s = (String) this.object.get(key); |
|
394 if (s == null) { |
|
395 return null; |
|
396 } |
|
397 return Utils.hex2Byte(s); |
|
398 } |
|
399 } |