/* * Copyright (c) 2014 Oculus Info Inc. * http://www.oculusinfo.com/ * * Released under the MIT License. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.oculusinfo.binning.util; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** * @author jandre * */ public class JsonUtilities { private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtilities.class); private static Object getJSONNull () { try { return new JSONObject("{a: null}").get("a"); } catch (JSONException e) { LOGGER.error("Can't come up with JSON null value"); return null; } } private static final Object JSON_NULL = getJSONNull(); /** * Clone a JSON object and all its child objects */ public static JSONObject deepClone (JSONObject source) { if (null == source) return null; try { JSONObject clone = new JSONObject(); String[] keys = JSONObject.getNames(source); if (null != keys) { for (String key: keys) { Object value = source.opt(key); if (value instanceof JSONObject) { JSONObject valueClone = deepClone((JSONObject) value); clone.put(key, valueClone); } else if (value instanceof JSONArray) { JSONArray valueClone = deepClone((JSONArray) value); clone.put(key, valueClone); } else { clone.put(key, value); } } } return clone; } catch (JSONException e) { LOGGER.error("Weird JSON exception cloning object", e); return null; } } /** * Clone a JSON array and all its child objects */ public static JSONArray deepClone (JSONArray source) { if (null == source) return null; try { JSONArray clone = new JSONArray(); for (int i=0; i<source.length(); ++i) { if (!source.isNull(i)) { Object value = source.opt(i); if (value instanceof JSONObject) { JSONObject valueClone = deepClone((JSONObject) value); clone.put(i, valueClone); } else if (value instanceof JSONArray) { JSONArray valueClone = deepClone((JSONArray) value); clone.put(i, valueClone); } else { clone.put(i, value); } } } return clone; } catch (JSONException e) { LOGGER.error("Weird JSON exception cloning object", e); return null; } } /** * Overlays one JSON object, in place, over another, deeply. * * @param base The object to alter * @param overlay The object defining how the base will be altered. * @return The base object, with the overlay now overlaid upon it. */ public static JSONObject overlayInPlace (JSONObject base, JSONObject overlay) { if (null == overlay) return base; if (null == base) return deepClone(overlay); try { String[] names = JSONObject.getNames(overlay); if (null == names) names = new String[0]; for (String key: names) { Object value = overlay.opt(key); if (value instanceof JSONObject) { if (base.has(key) && base.get(key) instanceof JSONObject) { overlayInPlace((JSONObject) base.get(key), (JSONObject) value); } else { base.put(key, deepClone((JSONObject) value)); } } else if (value instanceof JSONArray) { if (base.has(key) && base.get(key) instanceof JSONArray) { base.put(key, overlay((JSONArray) base.get(key), (JSONArray) value)); } else { base.put(key, deepClone((JSONArray) value)); } } else { base.put(key, value); } } return base; } catch (JSONException e) { LOGGER.error("Weird JSON exception overlaying object", e); return null; } } /** * Takes a JSON object, looks for any keys that have periods in them, and expands those into * multi-level objects. * * So: * * <pre> * { "name.first": "Alice", "name.middle": "Barbara", "name.last": "Cavendish" } * </pre> * * becomes: * * <pre> * { "name": { "first": "Alice", "middle": "Barbara", "last": "Cavendish" } } * </pre> * * @param base The JSON object to expand * @return The same JSON object, with keys expanded. * @throws JSONException */ public static JSONObject expandKeysInPlace (JSONObject base) throws JSONException { String[] names = JSONObject.getNames(base); if (null == names) names = new String[0]; for (String key: names) { Object value = base.get(key); // If our value is a JSON object or array, expend recursively if (value instanceof JSONObject) expandKeysInPlace((JSONObject) value); if (value instanceof JSONArray) expandKeysInPlace((JSONArray) value); // If this key is expandable, expand it String[] subKeys = key.split("\\."); if (subKeys.length > 1) { base.remove(key); JSONObject target = base; // Put in intermediate objects for (int i=0; i<subKeys.length-1; ++i) { if (!target.has(subKeys[i])) target.put(subKeys[i], new JSONObject()); if (!(target.get(subKeys[i]) instanceof JSONObject)) throw new JSONException("Impossible to expand keys - location "+subKeys[i]+" already exists, and is not an object."); target = target.getJSONObject(subKeys[i]); } // Put in our object target.put(subKeys[subKeys.length-1], value); } } return base; } /** * Takes a JSON array, looks for any internal keys that have periods in them, and expands * those into multi-level objects. * * So: * * <pre> * [ * { "name.first": "Alice", "name.middle": "Barbara", "name.last": "Cavendish" }, * { "name.first": "Dave", "name.middle": "Ezekiel", "name.last": "Filmore" } * ] * </pre> * * becomes: * * <pre> * [ * { "name": { "first": "Alice", "middle": "Barbara", "last": "Cavendish" } }, * { "name": { "first": "Dave", "middle": "Ezekiel", "last": "Filmore" } } * ] * </pre> * * @param base The JSON object to expand * @return The same JSON object, with keys expanded. * @throws JSONException */ public static JSONArray expandKeysInPlace (JSONArray base) throws JSONException { for (int i=0; i<base.length(); ++i) { if (!base.isNull(i)) { Object value = base.get(i); // If our value is a JSON object or array, expend recursively if (value instanceof JSONObject) expandKeysInPlace((JSONObject) value); if (value instanceof JSONArray) expandKeysInPlace((JSONArray) value); } } return base; } /** * Overlays one JSON array over another, deeply. This does not work in * place, but passes back a new array * * @param base * The array to alter * @param overlay * The array defining how the base will be altered. * @return The base array, with the overlay now overlaid upon it. */ public static JSONArray overlay (JSONArray base, JSONArray overlay) { if (null == overlay) return base; if (null == base) return deepClone(overlay); try { JSONArray result = new JSONArray(); // Overlay elements in both or just in the overlay for (int i=0; i<overlay.length(); ++i) { Object value = overlay.opt(i); Object baseValue = base.opt(i); if (JSON_NULL.equals(value)) { // Null array element; ignore, don't everlay if (baseValue instanceof JSONObject) { result.put(i, deepClone((JSONObject) baseValue)); } else if (baseValue instanceof JSONArray) { result.put(i, deepClone((JSONArray) baseValue)); } else { result.put(i, baseValue); } } else if (value instanceof JSONObject) { if (null != baseValue && baseValue instanceof JSONObject) { result.put(i, overlayInPlace((JSONObject) baseValue, (JSONObject) value)); } else { result.put(i, deepClone((JSONObject) value)); } } else if (value instanceof JSONArray) { if (null != baseValue && baseValue instanceof JSONArray) { result.put(i, overlay((JSONArray) baseValue, (JSONArray) value)); } else { result.put(i, deepClone((JSONArray) value)); } } else { result.put(i, value); } } return result; } catch (JSONException e) { LOGGER.error("Weird JSON exception overlaying "+overlay+" on "+base, e); return null; } } /** * Converts a {@link JSONObject} into a {@link Map} of key-value pairs. * This iterates through the tree and converts all {@link JSONObject}s * into their equivalent map, and converts {@link JSONArray}s into * {@link List}s. * * @param jsonObj * @return * Returns a map with the same */ public static Map<String, Object> jsonObjToMap(JSONObject jsonObj) { Map<String, Object> map = new HashMap<String, Object>(); Iterator<?> keys = jsonObj.keys(); while (keys.hasNext()) { String key = keys.next().toString(); Object obj = jsonObj.opt(key); if (obj instanceof JSONObject) { map.put(key, jsonObjToMap((JSONObject)obj)); } else if (obj instanceof JSONArray) { map.put(key, jsonArrayToList((JSONArray)obj)); } else { map.put(key, obj); } } return map; } /** * Converts a {@link JSONArray} into a {@link List} of values. * @param jsonList * @return * Returns a list of values */ public static List<Object> jsonArrayToList(JSONArray jsonList) { int numItems = jsonList.length(); List<Object> list = new ArrayList<Object>(numItems); for (int i = 0; i < numItems; i++) { Object obj = jsonList.opt(i); if (obj instanceof JSONObject) { list.add(jsonObjToMap((JSONObject)obj)); } else if (obj instanceof JSONArray) { list.add(jsonArrayToList((JSONArray)obj)); } else { list.add(obj); } } return list; } /** * Converts an object into a number. * @return * If the object is already a number then it just casts it. * If the object is a string, then it parses it as a double. * Otherwise the number returned is 0. */ public static Number getNumber(Object o) { Number val = 0; if (o instanceof Number) { val = (Number)o; } else if (o instanceof String) { val = Double.valueOf((String)o); } else if (o instanceof JSONArray) { //if the object is an array, then assume it only has one element that is the value JSONArray arr = (JSONArray)o; if (arr.length() == 1) { try { val = getNumber(arr.get(0)); } catch (JSONException e) { val = 0; } } } return val; } /** * Gets a name to use from an object. * If the object is a String, then it will treat the string as the name. * If the object is a {@link JSONObject}, then the name must be a parameter within the object. * If the object is a {@link JSONArray}, then there can only be a single element, which should * contain the name. * * @param params * @return * Returns the name for the object, or null if none can be found. */ public static String getName(Object params) { String name = null; if (params instanceof String) { name = (String)params; } else if (params instanceof JSONObject) { JSONObject transformObj = (JSONObject)params; try { name = (transformObj.has("name"))? transformObj.getString("name") : null; } catch (JSONException e) { name = null; } } else if (params instanceof JSONArray) { //if the transform params is an array, then it should only have one parameter. JSONArray vals = (JSONArray)params; if (vals.length() == 1) { try { name = getName(vals.get(0)); } catch (JSONException e) { name = null; } } else { name = null; } } return name; } /** * Simple getter for a {@link JSONObject} that handles exception handling, and * returns a default value in case there are any problems. * * @param obj * The {@link JSONObject} to query * @param keyName * The String name to query from the json object. * @param defaultVal * The default value to use if there are any problems. * @return * Returns the double value for the key name, or the default value. */ public static double getDoubleOrElse(JSONObject obj, String keyName, double defaultVal) { double val; try { val = (obj.has(keyName))? obj.getDouble(keyName) : defaultVal; } catch (JSONException e) { val = defaultVal; } return val; } /** * Transform a JSON object into a properties object, concatenating levels * into keys using a period. * * @param jsonObj * The JSON object to translate * @return The same data, in properties form */ public static Properties jsonObjToProperties (JSONObject jsonObj) { Properties properties = new Properties(); addProperties(jsonObj, properties, null); return properties; } private static void addProperties (JSONObject object, Properties properties, String keyBase) { Iterator<?> keys = object.keys(); while (keys.hasNext()) { String specificKey = keys.next().toString(); Object value = object.opt(specificKey); String key = (null == keyBase ? "" : keyBase+".") + specificKey; if (value instanceof JSONObject) { addProperties((JSONObject) value, properties, key); } else if (value instanceof JSONArray) { addProperties((JSONArray) value, properties, key); } else if (null != value) { properties.setProperty(key, value.toString()); } } } private static void addProperties (JSONArray array, Properties properties, String keyBase) { for (int i=0; i<array.length(); ++i) { String key = (null == keyBase ? "" : keyBase+".")+i; Object value = array.opt(i); if (value instanceof JSONObject) { addProperties((JSONObject) value, properties, key); } else if (value instanceof JSONArray) { addProperties((JSONArray) value, properties, key); } else if (null != value) { properties.setProperty(key, value.toString()); } } } /** * Transform a JSON object into a properties object, concatenating levels * into keys using a period. * * @param properties The properties object to translate * * @return The same data, in properties form */ public static JSONObject propertiesObjToJSON (Properties properties) { JSONObject json = new JSONObject(); for (String key: properties.stringPropertyNames()) { try { addKey(json, key, properties.getProperty(key)); } catch (JSONException e) { LOGGER.warn("Error transfering property {} from properties file to json", key, e); } } return json; } /** * Transform a string -> string map into a json object, nesting levels * based on a period. * * @param map The string -> string map to translate * * @return The same data, as a JSON object */ public static JSONObject mapToJSON (Map<String, String> map) { Properties properties = new Properties(); properties.putAll(map); return propertiesObjToJSON(properties); } private static void addKey (JSONObject json, String key, String value) throws JSONException { int keyBreak = key.indexOf("."); if (-1 == keyBreak) { // At leaf object. if (json.has(key)) { throw new JSONException("Duplicate key "+key); } // The value string itself may be valid JSON - try to parse it and add the JSON // object instead of the string if so. try { JSONObject obj = new JSONObject(value); json.put(key, obj); } catch (JSONException e){ json.put(key, value); } } else { String keyCAR = key.substring(0, keyBreak); String keyCDR = key.substring(keyBreak+1); String keyCADR; int cdrBreak = keyCDR.indexOf("."); if (-1 == cdrBreak) { keyCADR = keyCDR; } else { keyCADR = keyCDR.substring(0, cdrBreak); } // See if our next element can be an array element. boolean arrayOk; try { Integer.parseInt(keyCADR); arrayOk = true; } catch (NumberFormatException e) { arrayOk = false; } if (json.has(keyCAR)) { Object elt = json.get(keyCAR); if (elt instanceof JSONArray) { JSONArray arrayElt = (JSONArray) elt; if (arrayOk) { addKey(arrayElt, keyCDR, value); } else { JSONObject arrayTrans = new JSONObject(); for (int i=0; i<arrayElt.length(); ++i) { arrayTrans.put(""+i, arrayElt.get(i)); } json.put(keyCAR, arrayTrans); addKey(arrayTrans, keyCDR, value); } } else if (elt instanceof JSONObject) { addKey((JSONObject) elt, keyCDR, value); } else { throw new JSONException("Attempt to put both object and value in JSON object at key "+keyCAR); } } else { if (arrayOk) { JSONArray arrayElt = new JSONArray(); json.put(keyCAR, arrayElt); addKey(arrayElt, keyCDR, value); } else { JSONObject elt = new JSONObject(); json.put(keyCAR, elt); addKey(elt, keyCDR, value); } } } } private static void addKey (JSONArray json, String key, String value) throws JSONException { int keyBreak = key.indexOf("."); if (-1 == keyBreak) { // At leaf object. int index = Integer.parseInt(key); json.put(index, value); } else { String keyCAR = key.substring(0, keyBreak); String keyCDR = key.substring(keyBreak+1); String keyCADR; int cdrBreak = keyCDR.indexOf("."); if (-1 == cdrBreak) { keyCADR = keyCDR; } else { keyCADR = keyCDR.substring(0, cdrBreak); } // See if our next element can be an array element. boolean arrayOk; try { Integer.parseInt(keyCADR); arrayOk = true; } catch (NumberFormatException e) { arrayOk = false; } int index = Integer.parseInt(keyCAR); Object raw; try { raw = json.get(index); } catch (JSONException e) { raw = null; } if (raw instanceof JSONArray) { JSONArray arrayElt = (JSONArray) raw; if (arrayOk) { addKey(arrayElt, keyCDR, value); } else { JSONObject arrayTrans = new JSONObject(); for (int i=0; i<arrayElt.length(); ++i) { arrayTrans.put(""+i, arrayElt.get(i)); } json.put(index, arrayTrans); addKey(arrayTrans, keyCDR, value); } } else if (raw instanceof JSONObject) { addKey((JSONObject) raw, keyCDR, value); } else { if (arrayOk) { JSONArray arrayElt = new JSONArray(); json.put(index, arrayElt); addKey(arrayElt, keyCDR, value); } else { JSONObject elt = new JSONObject(); json.put(index, elt); addKey(elt, keyCDR, value); } } } } /** * Checks to see if supplied string is valid JSON * @param str candiate JSON string * @return <code>true</code> if valid JSON, <code>false</code> otherwise. */ public static boolean isJSON(String str) { try { new JSONObject(str); return true; } catch (JSONException e) { return false; } } }