/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.sync;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
/**
* Extend JSONObject to do little things, like, y'know, accessing members.
*
* @author rnewman
*
*/
public class ExtendedJSONObject {
public JSONObject object;
/**
* Return a JSONParser instance for immediate use.
*
* JSONParser is not thread-safe, so we return a new instance
* each call. This is extremely inefficient in execution time and especially
* memory use -- each instance allocates a 16kb temporary buffer -- and we
* hope to improve matters eventually.
*/
protected static JSONParser getJSONParser() {
return new JSONParser();
}
/**
* Parse a JSON encoded string.
*
* @param in Reader over a JSON-encoded input to parse; not
* necessarily a JSON object.
* @return a regular Java Object.
* @throws ParseException
* @throws IOException
*/
protected static Object parseRaw(Reader in) throws ParseException, IOException {
try {
return getJSONParser().parse(in);
} catch (Error e) {
// Don't be stupid, org.json.simple. Bug 1042929.
throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION, e);
}
}
/**
* Parse a JSON encoded string.
*
* You should prefer the streaming interface {@link #parseRaw(Reader)}.
*
* @param input JSON-encoded input string to parse; not necessarily a JSON object.
* @return a regular Java Object.
* @throws ParseException
*/
protected static Object parseRaw(String input) throws ParseException {
try {
return getJSONParser().parse(input);
} catch (Error e) {
// Don't be stupid, org.json.simple. Bug 1042929.
throw new ParseException(ParseException.ERROR_UNEXPECTED_EXCEPTION, e);
}
}
/**
* Helper method to get a JSON array from a stream.
*
* @param in Reader over a JSON-encoded array to parse.
* @throws ParseException
* @throws IOException
* @throws NonArrayJSONException if the object is valid JSON, but not an array.
*/
public static JSONArray parseJSONArray(Reader in)
throws IOException, ParseException, NonArrayJSONException {
Object o = parseRaw(in);
if (o == null) {
return null;
}
if (o instanceof JSONArray) {
return (JSONArray) o;
}
throw new NonArrayJSONException("value must be a JSON array");
}
/**
* Helper method to get a JSON array from a string.
*
* You should prefer the stream interface {@link #parseJSONArray(Reader)}.
*
* @param jsonString input.
* @throws ParseException
* @throws IOException
* @throws NonArrayJSONException if the object is valid JSON, but not an array.
*/
public static JSONArray parseJSONArray(String jsonString)
throws IOException, ParseException, NonArrayJSONException {
Object o = parseRaw(jsonString);
if (o == null) {
return null;
}
if (o instanceof JSONArray) {
return (JSONArray) o;
}
throw new NonArrayJSONException("value must be a JSON array");
}
/**
* Helper method to get a JSON object from a stream.
*
* @param in input {@link Reader}.
* @throws ParseException
* @throws IOException
* @throws NonArrayJSONException if the object is valid JSON, but not an object.
*/
public static ExtendedJSONObject parseJSONObject(Reader in)
throws IOException, ParseException, NonObjectJSONException {
return new ExtendedJSONObject(in);
}
/**
* Helper method to get a JSON object from a string.
*
* You should prefer the stream interface {@link #parseJSONObject(Reader)}.
*
* @param jsonString input.
* @throws ParseException
* @throws IOException
* @throws NonObjectJSONException if the object is valid JSON, but not an object.
*/
public static ExtendedJSONObject parseJSONObject(String jsonString)
throws IOException, ParseException, NonObjectJSONException {
return new ExtendedJSONObject(jsonString);
}
/**
* Helper method to get a JSON object from a UTF-8 byte array.
*
* @param in UTF-8 bytes.
* @throws ParseException
* @throws NonObjectJSONException if the object is valid JSON, but not an object.
* @throws IOException
*/
public static ExtendedJSONObject parseUTF8AsJSONObject(byte[] in)
throws ParseException, NonObjectJSONException, IOException {
return parseJSONObject(new String(in, "UTF-8"));
}
public ExtendedJSONObject() {
this.object = new JSONObject();
}
public ExtendedJSONObject(JSONObject o) {
this.object = o;
}
public ExtendedJSONObject deepCopy() {
final ExtendedJSONObject out = new ExtendedJSONObject();
@SuppressWarnings("unchecked")
final Set> entries = this.object.entrySet();
for (Map.Entry entry : entries) {
final String key = entry.getKey();
final Object value = entry.getValue();
if (value instanceof JSONArray) {
// Oh god.
try {
out.put(key, new JSONParser().parse(((JSONArray) value).toJSONString()));
} catch (ParseException e) {
// This should never occur, because we're round-tripping.
}
continue;
}
if (value instanceof JSONObject) {
out.put(key, new ExtendedJSONObject((JSONObject) value).deepCopy().object);
continue;
}
if (value instanceof ExtendedJSONObject) {
out.put(key, ((ExtendedJSONObject) value).deepCopy());
continue;
}
// Oh well.
out.put(key, value);
}
return out;
}
public ExtendedJSONObject(Reader in) throws IOException, ParseException, NonObjectJSONException {
if (in == null) {
this.object = new JSONObject();
return;
}
Object obj = parseRaw(in);
if (obj instanceof JSONObject) {
this.object = ((JSONObject) obj);
} else {
throw new NonObjectJSONException("value must be a JSON object");
}
}
public ExtendedJSONObject(String jsonString) throws IOException, ParseException, NonObjectJSONException {
this(jsonString == null ? null : new StringReader(jsonString));
}
@Override
public ExtendedJSONObject clone() {
return new ExtendedJSONObject((JSONObject) this.object.clone());
}
// Passthrough methods.
public Object get(String key) {
return this.object.get(key);
}
public long getLong(String key, long def) {
if (!object.containsKey(key)) {
return def;
}
Long val = getLong(key);
if (val == null) {
return def;
}
return val.longValue();
}
public Long getLong(String key) {
return (Long) this.get(key);
}
public String getString(String key) {
return (String) this.get(key);
}
public Boolean getBoolean(String key) {
return (Boolean) this.get(key);
}
/**
* Return an Integer if the value for this key is an Integer, Long, or String
* that can be parsed as a base 10 Integer.
* Passes through null.
*
* @throws NumberFormatException
*/
public Integer getIntegerSafely(String key) throws NumberFormatException {
Object val = this.object.get(key);
if (val == null) {
return null;
}
if (val instanceof Integer) {
return (Integer) val;
}
if (val instanceof Long) {
return ((Long) val).intValue();
}
if (val instanceof String) {
return Integer.parseInt((String) val, 10);
}
throw new NumberFormatException("Expecting Integer, got " + val.getClass());
}
/**
* Return a server timestamp value as milliseconds since epoch.
*
* @param key
* @return A Long, or null if the value is non-numeric or doesn't exist.
*/
public Long getTimestamp(String key) {
Object val = this.object.get(key);
// This is absurd.
if (val instanceof Double) {
double millis = ((Double) val) * 1000;
return Double.valueOf(millis).longValue();
}
if (val instanceof Float) {
double millis = ((Float) val).doubleValue() * 1000;
return Double.valueOf(millis).longValue();
}
if (val instanceof Number) {
// Must be an integral number.
return ((Number) val).longValue() * 1000;
}
return null;
}
public boolean containsKey(String key) {
return this.object.containsKey(key);
}
public String toJSONString() {
return this.object.toJSONString();
}
@Override
public String toString() {
return this.object.toString();
}
public void put(String key, Object value) {
@SuppressWarnings("unchecked")
Map