michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.sync; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.Reader; michael@0: import java.io.StringReader; michael@0: import java.util.Map; michael@0: import java.util.Map.Entry; michael@0: import java.util.Set; michael@0: michael@0: import org.json.simple.JSONArray; michael@0: import org.json.simple.JSONObject; michael@0: import org.json.simple.parser.JSONParser; michael@0: import org.json.simple.parser.ParseException; michael@0: import org.mozilla.apache.commons.codec.binary.Base64; michael@0: import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; michael@0: michael@0: /** michael@0: * Extend JSONObject to do little things, like, y'know, accessing members. michael@0: * michael@0: * @author rnewman michael@0: * michael@0: */ michael@0: public class ExtendedJSONObject { michael@0: michael@0: public JSONObject object; michael@0: michael@0: /** michael@0: * Return a JSONParser instance for immediate use. michael@0: *

michael@0: * JSONParser is not thread-safe, so we return a new instance michael@0: * each call. This is extremely inefficient in execution time and especially michael@0: * memory use -- each instance allocates a 16kb temporary buffer -- and we michael@0: * hope to improve matters eventually. michael@0: */ michael@0: protected static JSONParser getJSONParser() { michael@0: return new JSONParser(); michael@0: } michael@0: michael@0: /** michael@0: * Parse a JSON encoded string. michael@0: * michael@0: * @param in Reader over a JSON-encoded input to parse; not michael@0: * necessarily a JSON object. michael@0: * @return a regular Java Object. michael@0: * @throws ParseException michael@0: * @throws IOException michael@0: */ michael@0: protected static Object parseRaw(Reader in) throws ParseException, IOException { michael@0: try { michael@0: return getJSONParser().parse(in); michael@0: } catch (Error e) { michael@0: // Don't be stupid, org.json.simple. Bug 1042929. michael@0: throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Parse a JSON encoded string. michael@0: *

michael@0: * You should prefer the streaming interface {@link #parseRaw(Reader)}. michael@0: * michael@0: * @param input JSON-encoded input string to parse; not necessarily a JSON object. michael@0: * @return a regular Java Object. michael@0: * @throws ParseException michael@0: */ michael@0: protected static Object parseRaw(String input) throws ParseException { michael@0: try { michael@0: return getJSONParser().parse(input); michael@0: } catch (Error e) { michael@0: // Don't be stupid, org.json.simple. Bug 1042929. michael@0: throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Helper method to get a JSON array from a stream. michael@0: * michael@0: * @param in Reader over a JSON-encoded array to parse. michael@0: * @throws ParseException michael@0: * @throws IOException michael@0: * @throws NonArrayJSONException if the object is valid JSON, but not an array. michael@0: */ michael@0: public static JSONArray parseJSONArray(Reader in) michael@0: throws IOException, ParseException, NonArrayJSONException { michael@0: Object o = parseRaw(in); michael@0: michael@0: if (o == null) { michael@0: return null; michael@0: } michael@0: michael@0: if (o instanceof JSONArray) { michael@0: return (JSONArray) o; michael@0: } michael@0: michael@0: throw new NonArrayJSONException("value must be a JSON array"); michael@0: } michael@0: michael@0: /** michael@0: * Helper method to get a JSON array from a string. michael@0: *

michael@0: * You should prefer the stream interface {@link #parseJSONArray(Reader)}. michael@0: * michael@0: * @param jsonString input. michael@0: * @throws ParseException michael@0: * @throws IOException michael@0: * @throws NonArrayJSONException if the object is valid JSON, but not an array. michael@0: */ michael@0: public static JSONArray parseJSONArray(String jsonString) michael@0: throws IOException, ParseException, NonArrayJSONException { michael@0: Object o = parseRaw(jsonString); michael@0: michael@0: if (o == null) { michael@0: return null; michael@0: } michael@0: michael@0: if (o instanceof JSONArray) { michael@0: return (JSONArray) o; michael@0: } michael@0: michael@0: throw new NonArrayJSONException("value must be a JSON array"); michael@0: } michael@0: michael@0: /** michael@0: * Helper method to get a JSON object from a stream. michael@0: * michael@0: * @param in input {@link Reader}. michael@0: * @throws ParseException michael@0: * @throws IOException michael@0: * @throws NonArrayJSONException if the object is valid JSON, but not an object. michael@0: */ michael@0: public static ExtendedJSONObject parseJSONObject(Reader in) michael@0: throws IOException, ParseException, NonObjectJSONException { michael@0: return new ExtendedJSONObject(in); michael@0: } michael@0: michael@0: /** michael@0: * Helper method to get a JSON object from a string. michael@0: *

michael@0: * You should prefer the stream interface {@link #parseJSONObject(Reader)}. michael@0: * michael@0: * @param jsonString input. michael@0: * @throws ParseException michael@0: * @throws IOException michael@0: * @throws NonObjectJSONException if the object is valid JSON, but not an object. michael@0: */ michael@0: public static ExtendedJSONObject parseJSONObject(String jsonString) michael@0: throws IOException, ParseException, NonObjectJSONException { michael@0: return new ExtendedJSONObject(jsonString); michael@0: } michael@0: michael@0: /** michael@0: * Helper method to get a JSON object from a UTF-8 byte array. michael@0: * michael@0: * @param in UTF-8 bytes. michael@0: * @throws ParseException michael@0: * @throws NonObjectJSONException if the object is valid JSON, but not an object. michael@0: * @throws IOException michael@0: */ michael@0: public static ExtendedJSONObject parseUTF8AsJSONObject(byte[] in) michael@0: throws ParseException, NonObjectJSONException, IOException { michael@0: return parseJSONObject(new String(in, "UTF-8")); michael@0: } michael@0: michael@0: public ExtendedJSONObject() { michael@0: this.object = new JSONObject(); michael@0: } michael@0: michael@0: public ExtendedJSONObject(JSONObject o) { michael@0: this.object = o; michael@0: } michael@0: michael@0: public ExtendedJSONObject(Reader in) throws IOException, ParseException, NonObjectJSONException { michael@0: if (in == null) { michael@0: this.object = new JSONObject(); michael@0: return; michael@0: } michael@0: michael@0: Object obj = parseRaw(in); michael@0: if (obj instanceof JSONObject) { michael@0: this.object = ((JSONObject) obj); michael@0: } else { michael@0: throw new NonObjectJSONException("value must be a JSON object"); michael@0: } michael@0: } michael@0: michael@0: public ExtendedJSONObject(String jsonString) throws IOException, ParseException, NonObjectJSONException { michael@0: this(jsonString == null ? null : new StringReader(jsonString)); michael@0: } michael@0: michael@0: // Passthrough methods. michael@0: public Object get(String key) { michael@0: return this.object.get(key); michael@0: } michael@0: michael@0: public Long getLong(String key) { michael@0: return (Long) this.get(key); michael@0: } michael@0: michael@0: public String getString(String key) { michael@0: return (String) this.get(key); michael@0: } michael@0: michael@0: public Boolean getBoolean(String key) { michael@0: return (Boolean) this.get(key); michael@0: } michael@0: michael@0: /** michael@0: * Return an Integer if the value for this key is an Integer, Long, or String michael@0: * that can be parsed as a base 10 Integer. michael@0: * Passes through null. michael@0: * michael@0: * @throws NumberFormatException michael@0: */ michael@0: public Integer getIntegerSafely(String key) throws NumberFormatException { michael@0: Object val = this.object.get(key); michael@0: if (val == null) { michael@0: return null; michael@0: } michael@0: if (val instanceof Integer) { michael@0: return (Integer) val; michael@0: } michael@0: if (val instanceof Long) { michael@0: return Integer.valueOf(((Long) val).intValue()); michael@0: } michael@0: if (val instanceof String) { michael@0: return Integer.parseInt((String) val, 10); michael@0: } michael@0: throw new NumberFormatException("Expecting Integer, got " + val.getClass()); michael@0: } michael@0: michael@0: /** michael@0: * Return a server timestamp value as milliseconds since epoch. michael@0: * michael@0: * @param key michael@0: * @return A Long, or null if the value is non-numeric or doesn't exist. michael@0: */ michael@0: public Long getTimestamp(String key) { michael@0: Object val = this.object.get(key); michael@0: michael@0: // This is absurd. michael@0: if (val instanceof Double) { michael@0: double millis = ((Double) val).doubleValue() * 1000; michael@0: return Double.valueOf(millis).longValue(); michael@0: } michael@0: if (val instanceof Float) { michael@0: double millis = ((Float) val).doubleValue() * 1000; michael@0: return Double.valueOf(millis).longValue(); michael@0: } michael@0: if (val instanceof Number) { michael@0: // Must be an integral number. michael@0: return ((Number) val).longValue() * 1000; michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: public boolean containsKey(String key) { michael@0: return this.object.containsKey(key); michael@0: } michael@0: michael@0: public String toJSONString() { michael@0: return this.object.toJSONString(); michael@0: } michael@0: michael@0: public String toString() { michael@0: return this.object.toString(); michael@0: } michael@0: michael@0: public void put(String key, Object value) { michael@0: @SuppressWarnings("unchecked") michael@0: Map map = this.object; michael@0: map.put(key, value); michael@0: } michael@0: michael@0: @SuppressWarnings({ "unchecked", "rawtypes" }) michael@0: public void putAll(Map map) { michael@0: this.object.putAll(map); michael@0: } michael@0: michael@0: /** michael@0: * Remove key-value pair from JSONObject. michael@0: * michael@0: * @param key michael@0: * to be removed. michael@0: * @return true if key exists and was removed, false otherwise. michael@0: */ michael@0: public boolean remove(String key) { michael@0: Object res = this.object.remove(key); michael@0: return (res != null); michael@0: } michael@0: michael@0: public ExtendedJSONObject getObject(String key) throws NonObjectJSONException { michael@0: Object o = this.object.get(key); michael@0: if (o == null) { michael@0: return null; michael@0: } michael@0: if (o instanceof ExtendedJSONObject) { michael@0: return (ExtendedJSONObject) o; michael@0: } michael@0: if (o instanceof JSONObject) { michael@0: return new ExtendedJSONObject((JSONObject) o); michael@0: } michael@0: throw new NonObjectJSONException("key must be a JSON object: " + key); michael@0: } michael@0: michael@0: @SuppressWarnings("unchecked") michael@0: public Set> entrySet() { michael@0: return this.object.entrySet(); michael@0: } michael@0: michael@0: @SuppressWarnings("unchecked") michael@0: public Set keySet() { michael@0: return this.object.keySet(); michael@0: } michael@0: michael@0: public org.json.simple.JSONArray getArray(String key) throws NonArrayJSONException { michael@0: Object o = this.object.get(key); michael@0: if (o == null) { michael@0: return null; michael@0: } michael@0: if (o instanceof JSONArray) { michael@0: return (JSONArray) o; michael@0: } michael@0: throw new NonArrayJSONException("key must be a JSON array: " + key); michael@0: } michael@0: michael@0: public int size() { michael@0: return this.object.size(); michael@0: } michael@0: michael@0: @Override michael@0: public int hashCode() { michael@0: if (this.object == null) { michael@0: return getClass().hashCode(); michael@0: } michael@0: return this.object.hashCode() ^ getClass().hashCode(); michael@0: } michael@0: michael@0: @Override michael@0: public boolean equals(Object o) { michael@0: if (o == null || !(o instanceof ExtendedJSONObject)) { michael@0: return false; michael@0: } michael@0: if (o == this) { michael@0: return true; michael@0: } michael@0: ExtendedJSONObject other = (ExtendedJSONObject) o; michael@0: if (this.object == null) { michael@0: return other.object == null; michael@0: } michael@0: return this.object.equals(other.object); michael@0: } michael@0: michael@0: /** michael@0: * Throw if keys are missing or values have wrong types. michael@0: * michael@0: * @param requiredFields list of required keys. michael@0: * @param requiredFieldClass class that values must be coercable to; may be null, which means don't check. michael@0: * @throws UnexpectedJSONException michael@0: */ michael@0: public void throwIfFieldsMissingOrMisTyped(String[] requiredFields, Class requiredFieldClass) throws BadRequiredFieldJSONException { michael@0: // Defensive as possible: verify object has expected key(s) with string value. michael@0: for (String k : requiredFields) { michael@0: Object value = get(k); michael@0: if (value == null) { michael@0: throw new BadRequiredFieldJSONException("Expected key not present in result: " + k); michael@0: } michael@0: if (requiredFieldClass != null && !(requiredFieldClass.isInstance(value))) { michael@0: throw new BadRequiredFieldJSONException("Value for key not an instance of " + requiredFieldClass + ": " + k); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Return a base64-encoded string value as a byte array. michael@0: */ michael@0: public byte[] getByteArrayBase64(String key) { michael@0: String s = (String) this.object.get(key); michael@0: if (s == null) { michael@0: return null; michael@0: } michael@0: return Base64.decodeBase64(s); michael@0: } michael@0: michael@0: /** michael@0: * Return a hex-encoded string value as a byte array. michael@0: */ michael@0: public byte[] getByteArrayHex(String key) { michael@0: String s = (String) this.object.get(key); michael@0: if (s == null) { michael@0: return null; michael@0: } michael@0: return Utils.hex2Byte(s); michael@0: } michael@0: }