mobile/android/base/sync/ExtendedJSONObject.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial