1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/ExtendedJSONObject.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,399 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync; 1.9 + 1.10 +import java.io.IOException; 1.11 +import java.io.Reader; 1.12 +import java.io.StringReader; 1.13 +import java.util.Map; 1.14 +import java.util.Map.Entry; 1.15 +import java.util.Set; 1.16 + 1.17 +import org.json.simple.JSONArray; 1.18 +import org.json.simple.JSONObject; 1.19 +import org.json.simple.parser.JSONParser; 1.20 +import org.json.simple.parser.ParseException; 1.21 +import org.mozilla.apache.commons.codec.binary.Base64; 1.22 +import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; 1.23 + 1.24 +/** 1.25 + * Extend JSONObject to do little things, like, y'know, accessing members. 1.26 + * 1.27 + * @author rnewman 1.28 + * 1.29 + */ 1.30 +public class ExtendedJSONObject { 1.31 + 1.32 + public JSONObject object; 1.33 + 1.34 + /** 1.35 + * Return a <code>JSONParser</code> instance for immediate use. 1.36 + * <p> 1.37 + * <code>JSONParser</code> is not thread-safe, so we return a new instance 1.38 + * each call. This is extremely inefficient in execution time and especially 1.39 + * memory use -- each instance allocates a 16kb temporary buffer -- and we 1.40 + * hope to improve matters eventually. 1.41 + */ 1.42 + protected static JSONParser getJSONParser() { 1.43 + return new JSONParser(); 1.44 + } 1.45 + 1.46 + /** 1.47 + * Parse a JSON encoded string. 1.48 + * 1.49 + * @param in <code>Reader</code> over a JSON-encoded input to parse; not 1.50 + * necessarily a JSON object. 1.51 + * @return a regular Java <code>Object</code>. 1.52 + * @throws ParseException 1.53 + * @throws IOException 1.54 + */ 1.55 + protected static Object parseRaw(Reader in) throws ParseException, IOException { 1.56 + try { 1.57 + return getJSONParser().parse(in); 1.58 + } catch (Error e) { 1.59 + // Don't be stupid, org.json.simple. Bug 1042929. 1.60 + throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION); 1.61 + } 1.62 + } 1.63 + 1.64 + /** 1.65 + * Parse a JSON encoded string. 1.66 + * <p> 1.67 + * You should prefer the streaming interface {@link #parseRaw(Reader)}. 1.68 + * 1.69 + * @param input JSON-encoded input string to parse; not necessarily a JSON object. 1.70 + * @return a regular Java <code>Object</code>. 1.71 + * @throws ParseException 1.72 + */ 1.73 + protected static Object parseRaw(String input) throws ParseException { 1.74 + try { 1.75 + return getJSONParser().parse(input); 1.76 + } catch (Error e) { 1.77 + // Don't be stupid, org.json.simple. Bug 1042929. 1.78 + throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION); 1.79 + } 1.80 + } 1.81 + 1.82 + /** 1.83 + * Helper method to get a JSON array from a stream. 1.84 + * 1.85 + * @param in <code>Reader</code> over a JSON-encoded array to parse. 1.86 + * @throws ParseException 1.87 + * @throws IOException 1.88 + * @throws NonArrayJSONException if the object is valid JSON, but not an array. 1.89 + */ 1.90 + public static JSONArray parseJSONArray(Reader in) 1.91 + throws IOException, ParseException, NonArrayJSONException { 1.92 + Object o = parseRaw(in); 1.93 + 1.94 + if (o == null) { 1.95 + return null; 1.96 + } 1.97 + 1.98 + if (o instanceof JSONArray) { 1.99 + return (JSONArray) o; 1.100 + } 1.101 + 1.102 + throw new NonArrayJSONException("value must be a JSON array"); 1.103 + } 1.104 + 1.105 + /** 1.106 + * Helper method to get a JSON array from a string. 1.107 + * <p> 1.108 + * You should prefer the stream interface {@link #parseJSONArray(Reader)}. 1.109 + * 1.110 + * @param jsonString input. 1.111 + * @throws ParseException 1.112 + * @throws IOException 1.113 + * @throws NonArrayJSONException if the object is valid JSON, but not an array. 1.114 + */ 1.115 + public static JSONArray parseJSONArray(String jsonString) 1.116 + throws IOException, ParseException, NonArrayJSONException { 1.117 + Object o = parseRaw(jsonString); 1.118 + 1.119 + if (o == null) { 1.120 + return null; 1.121 + } 1.122 + 1.123 + if (o instanceof JSONArray) { 1.124 + return (JSONArray) o; 1.125 + } 1.126 + 1.127 + throw new NonArrayJSONException("value must be a JSON array"); 1.128 + } 1.129 + 1.130 + /** 1.131 + * Helper method to get a JSON object from a stream. 1.132 + * 1.133 + * @param in input {@link Reader}. 1.134 + * @throws ParseException 1.135 + * @throws IOException 1.136 + * @throws NonArrayJSONException if the object is valid JSON, but not an object. 1.137 + */ 1.138 + public static ExtendedJSONObject parseJSONObject(Reader in) 1.139 + throws IOException, ParseException, NonObjectJSONException { 1.140 + return new ExtendedJSONObject(in); 1.141 + } 1.142 + 1.143 + /** 1.144 + * Helper method to get a JSON object from a string. 1.145 + * <p> 1.146 + * You should prefer the stream interface {@link #parseJSONObject(Reader)}. 1.147 + * 1.148 + * @param jsonString input. 1.149 + * @throws ParseException 1.150 + * @throws IOException 1.151 + * @throws NonObjectJSONException if the object is valid JSON, but not an object. 1.152 + */ 1.153 + public static ExtendedJSONObject parseJSONObject(String jsonString) 1.154 + throws IOException, ParseException, NonObjectJSONException { 1.155 + return new ExtendedJSONObject(jsonString); 1.156 + } 1.157 + 1.158 + /** 1.159 + * Helper method to get a JSON object from a UTF-8 byte array. 1.160 + * 1.161 + * @param in UTF-8 bytes. 1.162 + * @throws ParseException 1.163 + * @throws NonObjectJSONException if the object is valid JSON, but not an object. 1.164 + * @throws IOException 1.165 + */ 1.166 + public static ExtendedJSONObject parseUTF8AsJSONObject(byte[] in) 1.167 + throws ParseException, NonObjectJSONException, IOException { 1.168 + return parseJSONObject(new String(in, "UTF-8")); 1.169 + } 1.170 + 1.171 + public ExtendedJSONObject() { 1.172 + this.object = new JSONObject(); 1.173 + } 1.174 + 1.175 + public ExtendedJSONObject(JSONObject o) { 1.176 + this.object = o; 1.177 + } 1.178 + 1.179 + public ExtendedJSONObject(Reader in) throws IOException, ParseException, NonObjectJSONException { 1.180 + if (in == null) { 1.181 + this.object = new JSONObject(); 1.182 + return; 1.183 + } 1.184 + 1.185 + Object obj = parseRaw(in); 1.186 + if (obj instanceof JSONObject) { 1.187 + this.object = ((JSONObject) obj); 1.188 + } else { 1.189 + throw new NonObjectJSONException("value must be a JSON object"); 1.190 + } 1.191 + } 1.192 + 1.193 + public ExtendedJSONObject(String jsonString) throws IOException, ParseException, NonObjectJSONException { 1.194 + this(jsonString == null ? null : new StringReader(jsonString)); 1.195 + } 1.196 + 1.197 + // Passthrough methods. 1.198 + public Object get(String key) { 1.199 + return this.object.get(key); 1.200 + } 1.201 + 1.202 + public Long getLong(String key) { 1.203 + return (Long) this.get(key); 1.204 + } 1.205 + 1.206 + public String getString(String key) { 1.207 + return (String) this.get(key); 1.208 + } 1.209 + 1.210 + public Boolean getBoolean(String key) { 1.211 + return (Boolean) this.get(key); 1.212 + } 1.213 + 1.214 + /** 1.215 + * Return an Integer if the value for this key is an Integer, Long, or String 1.216 + * that can be parsed as a base 10 Integer. 1.217 + * Passes through null. 1.218 + * 1.219 + * @throws NumberFormatException 1.220 + */ 1.221 + public Integer getIntegerSafely(String key) throws NumberFormatException { 1.222 + Object val = this.object.get(key); 1.223 + if (val == null) { 1.224 + return null; 1.225 + } 1.226 + if (val instanceof Integer) { 1.227 + return (Integer) val; 1.228 + } 1.229 + if (val instanceof Long) { 1.230 + return Integer.valueOf(((Long) val).intValue()); 1.231 + } 1.232 + if (val instanceof String) { 1.233 + return Integer.parseInt((String) val, 10); 1.234 + } 1.235 + throw new NumberFormatException("Expecting Integer, got " + val.getClass()); 1.236 + } 1.237 + 1.238 + /** 1.239 + * Return a server timestamp value as milliseconds since epoch. 1.240 + * 1.241 + * @param key 1.242 + * @return A Long, or null if the value is non-numeric or doesn't exist. 1.243 + */ 1.244 + public Long getTimestamp(String key) { 1.245 + Object val = this.object.get(key); 1.246 + 1.247 + // This is absurd. 1.248 + if (val instanceof Double) { 1.249 + double millis = ((Double) val).doubleValue() * 1000; 1.250 + return Double.valueOf(millis).longValue(); 1.251 + } 1.252 + if (val instanceof Float) { 1.253 + double millis = ((Float) val).doubleValue() * 1000; 1.254 + return Double.valueOf(millis).longValue(); 1.255 + } 1.256 + if (val instanceof Number) { 1.257 + // Must be an integral number. 1.258 + return ((Number) val).longValue() * 1000; 1.259 + } 1.260 + 1.261 + return null; 1.262 + } 1.263 + 1.264 + public boolean containsKey(String key) { 1.265 + return this.object.containsKey(key); 1.266 + } 1.267 + 1.268 + public String toJSONString() { 1.269 + return this.object.toJSONString(); 1.270 + } 1.271 + 1.272 + public String toString() { 1.273 + return this.object.toString(); 1.274 + } 1.275 + 1.276 + public void put(String key, Object value) { 1.277 + @SuppressWarnings("unchecked") 1.278 + Map<Object, Object> map = this.object; 1.279 + map.put(key, value); 1.280 + } 1.281 + 1.282 + @SuppressWarnings({ "unchecked", "rawtypes" }) 1.283 + public void putAll(Map map) { 1.284 + this.object.putAll(map); 1.285 + } 1.286 + 1.287 + /** 1.288 + * Remove key-value pair from JSONObject. 1.289 + * 1.290 + * @param key 1.291 + * to be removed. 1.292 + * @return true if key exists and was removed, false otherwise. 1.293 + */ 1.294 + public boolean remove(String key) { 1.295 + Object res = this.object.remove(key); 1.296 + return (res != null); 1.297 + } 1.298 + 1.299 + public ExtendedJSONObject getObject(String key) throws NonObjectJSONException { 1.300 + Object o = this.object.get(key); 1.301 + if (o == null) { 1.302 + return null; 1.303 + } 1.304 + if (o instanceof ExtendedJSONObject) { 1.305 + return (ExtendedJSONObject) o; 1.306 + } 1.307 + if (o instanceof JSONObject) { 1.308 + return new ExtendedJSONObject((JSONObject) o); 1.309 + } 1.310 + throw new NonObjectJSONException("key must be a JSON object: " + key); 1.311 + } 1.312 + 1.313 + @SuppressWarnings("unchecked") 1.314 + public Set<Entry<String, Object>> entrySet() { 1.315 + return this.object.entrySet(); 1.316 + } 1.317 + 1.318 + @SuppressWarnings("unchecked") 1.319 + public Set<String> keySet() { 1.320 + return this.object.keySet(); 1.321 + } 1.322 + 1.323 + public org.json.simple.JSONArray getArray(String key) throws NonArrayJSONException { 1.324 + Object o = this.object.get(key); 1.325 + if (o == null) { 1.326 + return null; 1.327 + } 1.328 + if (o instanceof JSONArray) { 1.329 + return (JSONArray) o; 1.330 + } 1.331 + throw new NonArrayJSONException("key must be a JSON array: " + key); 1.332 + } 1.333 + 1.334 + public int size() { 1.335 + return this.object.size(); 1.336 + } 1.337 + 1.338 + @Override 1.339 + public int hashCode() { 1.340 + if (this.object == null) { 1.341 + return getClass().hashCode(); 1.342 + } 1.343 + return this.object.hashCode() ^ getClass().hashCode(); 1.344 + } 1.345 + 1.346 + @Override 1.347 + public boolean equals(Object o) { 1.348 + if (o == null || !(o instanceof ExtendedJSONObject)) { 1.349 + return false; 1.350 + } 1.351 + if (o == this) { 1.352 + return true; 1.353 + } 1.354 + ExtendedJSONObject other = (ExtendedJSONObject) o; 1.355 + if (this.object == null) { 1.356 + return other.object == null; 1.357 + } 1.358 + return this.object.equals(other.object); 1.359 + } 1.360 + 1.361 + /** 1.362 + * Throw if keys are missing or values have wrong types. 1.363 + * 1.364 + * @param requiredFields list of required keys. 1.365 + * @param requiredFieldClass class that values must be coercable to; may be null, which means don't check. 1.366 + * @throws UnexpectedJSONException 1.367 + */ 1.368 + public void throwIfFieldsMissingOrMisTyped(String[] requiredFields, Class<?> requiredFieldClass) throws BadRequiredFieldJSONException { 1.369 + // Defensive as possible: verify object has expected key(s) with string value. 1.370 + for (String k : requiredFields) { 1.371 + Object value = get(k); 1.372 + if (value == null) { 1.373 + throw new BadRequiredFieldJSONException("Expected key not present in result: " + k); 1.374 + } 1.375 + if (requiredFieldClass != null && !(requiredFieldClass.isInstance(value))) { 1.376 + throw new BadRequiredFieldJSONException("Value for key not an instance of " + requiredFieldClass + ": " + k); 1.377 + } 1.378 + } 1.379 + } 1.380 + 1.381 + /** 1.382 + * Return a base64-encoded string value as a byte array. 1.383 + */ 1.384 + public byte[] getByteArrayBase64(String key) { 1.385 + String s = (String) this.object.get(key); 1.386 + if (s == null) { 1.387 + return null; 1.388 + } 1.389 + return Base64.decodeBase64(s); 1.390 + } 1.391 + 1.392 + /** 1.393 + * Return a hex-encoded string value as a byte array. 1.394 + */ 1.395 + public byte[] getByteArrayHex(String key) { 1.396 + String s = (String) this.object.get(key); 1.397 + if (s == null) { 1.398 + return null; 1.399 + } 1.400 + return Utils.hex2Byte(s); 1.401 + } 1.402 +}