/***************************************************************************
* Copyright (C) 2012 by H-Store Project *
* Brown University *
* Massachusetts Institute of Technology *
* Yale University *
* *
* http://hstore.cs.brown.edu/ *
* *
* 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 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 edu.brown.utils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONStringer;
import org.voltdb.VoltType;
import org.voltdb.catalog.CatalogType;
import org.voltdb.catalog.Database;
import org.voltdb.utils.VoltTypeUtil;
import edu.brown.catalog.CatalogKey;
import edu.brown.logging.LoggerUtil;
import edu.brown.logging.LoggerUtil.LoggerBoolean;
/**
* @author pavlo
*/
public abstract class JSONUtil {
private static final Logger LOG = Logger.getLogger(JSONUtil.class.getName());
private static final LoggerBoolean debug = new LoggerBoolean();
private static final LoggerBoolean trace = new LoggerBoolean();
static {
LoggerUtil.attachObserver(LOG, debug, trace);
}
private static final String JSON_CLASS_SUFFIX = "_class";
private static final Map<Class<?>, Field[]> SERIALIZABLE_FIELDS = new HashMap<Class<?>, Field[]>();
/**
* @param clazz
* @return
*/
public static Field[] getSerializableFields(Class<?> clazz, String... fieldsToExclude) {
Field ret[] = SERIALIZABLE_FIELDS.get(clazz);
if (ret == null) {
Collection<String> exclude = CollectionUtil.addAll(new HashSet<String>(), fieldsToExclude);
synchronized (SERIALIZABLE_FIELDS) {
ret = SERIALIZABLE_FIELDS.get(clazz);
if (ret == null) {
List<Field> fields = new ArrayList<Field>();
for (Field f : clazz.getFields()) {
int modifiers = f.getModifiers();
if (Modifier.isTransient(modifiers) == false &&
Modifier.isPublic(modifiers) == true &&
Modifier.isStatic(modifiers) == false &&
exclude.contains(f.getName()) == false) {
fields.add(f);
}
} // FOR
ret = fields.toArray(new Field[0]);
SERIALIZABLE_FIELDS.put(clazz, ret);
}
} // SYNCH
}
return (ret);
}
/**
* JSON Pretty Print
*
* @param json
* @return
* @throws JSONException
*/
public static String format(String json) {
try {
return (JSONUtil.format(new JSONObject(json)));
} catch (RuntimeException ex) {
throw ex;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* JSON Pretty Print
*
* @param <T>
* @param object
* @return
*/
public static <T extends JSONSerializable> String format(T object) {
JSONStringer stringer = new JSONStringer();
try {
if (object instanceof JSONObject)
return ((JSONObject) object).toString(2);
stringer.object();
object.toJSON(stringer);
stringer.endObject();
} catch (JSONException ex) {
throw new RuntimeException(ex);
}
return (JSONUtil.format(stringer.toString()));
}
public static String format(JSONObject o) {
try {
return o.toString(1);
} catch (JSONException ex) {
throw new RuntimeException(ex);
}
}
/**
* @param <T>
* @param object
* @return
*/
public static String toJSONString(Object object) {
JSONStringer stringer = new JSONStringer();
try {
if (object instanceof JSONSerializable) {
stringer.object();
((JSONSerializable) object).toJSON(stringer);
stringer.endObject();
} else if (object != null) {
Class<?> clazz = object.getClass();
// stringer.key(clazz.getSimpleName());
JSONUtil.writeFieldValue(stringer, clazz, object);
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
return (stringer.toString());
}
public static <T extends JSONSerializable> T fromJSONString(T t, String json) {
return (fromJSONString(t, null, json));
}
public static <T extends JSONSerializable> T fromJSONString(T t, Database catalog_db, String json) {
try {
JSONObject json_object = new JSONObject(json);
t.fromJSON(json_object, catalog_db);
} catch (JSONException ex) {
throw new RuntimeException("Failed to deserialize object " + t, ex);
}
return (t);
}
/**
* Write the contents of a JSONSerializable object out to a file on the
* local disk
*
* @param <T>
* @param object
* @param output_path
* @throws IOException
*/
public static <T extends JSONSerializable> void save(T object, File output_path) throws IOException {
if (debug.val)
LOG.debug("Writing out contents of " + object.getClass().getSimpleName() + " to '" + output_path + "'");
try {
FileUtil.makeDirIfNotExists(output_path.getParent());
String json = object.toJSONString();
FileUtil.writeStringToFile(output_path, format(json));
} catch (Exception ex) {
LOG.error("Failed to serialize the " + object.getClass().getSimpleName() + " file '" + output_path + "'", ex);
throw new IOException(ex);
}
}
/**
* Load in a JSONSerialable stored in a file
*
* @param <T>
* @param object
* @param output_path
* @throws Exception
*/
public static <T extends JSONSerializable> void load(T object, Database catalog_db, File input_path) throws IOException {
if (debug.val)
LOG.debug("Loading in serialized " + object.getClass().getSimpleName() + " from '" + input_path + "'");
String contents = FileUtil.readFile(input_path);
if (contents.isEmpty()) {
throw new IOException("The " + object.getClass().getSimpleName() + " file '" + input_path + "' is empty");
}
try {
object.fromJSON(new JSONObject(contents), catalog_db);
} catch (Exception ex) {
if (debug.val)
LOG.error("Failed to deserialize the " + object.getClass().getSimpleName() + " from file '" + input_path + "'", ex);
throw new IOException(ex);
}
if (debug.val)
LOG.debug("The loading of the " + object.getClass().getSimpleName() + " is complete");
}
/**
* For a given Enum, write out the contents of the corresponding field to
* the JSONObject We assume that the given object has matching fields that
* correspond to the Enum members, except that their names are lower case.
*
* @param <E>
* @param <T>
* @param stringer
* @param object
* @param base_class
* @param members
* @throws JSONException
*/
public static <E extends Enum<?>, T> void fieldsToJSON(JSONStringer stringer, T object, Class<? extends T> base_class, E members[]) throws JSONException {
try {
fieldsToJSON(stringer, object, base_class, ClassUtil.getFieldsFromMembersEnum(base_class, members));
} catch (NoSuchFieldException ex) {
throw new JSONException(ex);
}
}
/**
* For a given list of Fields, write out the contents of the corresponding
* field to the JSONObject The each of the JSONObject's elements will be the
* upper case version of the Field's name
*
* @param <T>
* @param stringer
* @param object
* @param base_class
* @param fields
* @throws JSONException
*/
public static <T> void fieldsToJSON(JSONStringer stringer, T object, Class<? extends T> base_class, Field fields[]) throws JSONException {
if (debug.val)
LOG.debug("Serializing out " + fields.length + " elements for " + base_class.getSimpleName());
for (Field f : fields) {
String json_key = f.getName().toUpperCase();
stringer.key(json_key);
try {
Class<?> f_class = f.getType();
Object f_value = f.get(object);
// Null
if (f_value == null) {
writeFieldValue(stringer, f_class, f_value);
// Maps
} else if (f_value instanceof Map) {
writeFieldValue(stringer, f_class, f_value);
// Everything else
} else {
writeFieldValue(stringer, f_class, f_value);
addClassForField(stringer, json_key, f_class, f_value);
}
} catch (Exception ex) {
throw new JSONException(ex);
}
} // FOR
}
/**
* @param stringer
* @param field_class
* @param field_value
* @throws JSONException
*/
public static void writeFieldValue(JSONStringer stringer, Class<?> field_class, Object field_value) throws JSONException {
// Null
if (field_value == null) {
if (debug.val)
LOG.debug("writeNullFieldValue(" + field_class + ", " + field_value + ")");
stringer.value(null);
}
// Collections
else if (ClassUtil.getInterfaces(field_class).contains(Collection.class)) {
if (debug.val)
LOG.debug("writeCollectionFieldValue(" + field_class + ", " + field_value + ")");
stringer.array();
for (Object value : (Collection<?>) field_value) {
if (value == null) {
stringer.value(null);
} else {
writeFieldValue(stringer, value.getClass(), value);
}
} // FOR
stringer.endArray();
}
// Maps
else if (field_value instanceof Map) {
if (debug.val)
LOG.debug("writeMapFieldValue(" + field_class + ", " + field_value + ")");
stringer.object();
for (Entry<?, ?> e : ((Map<?, ?>) field_value).entrySet()) {
// We can handle null keys
String key_value = null;
if (e.getKey() != null) {
// The key can't be a raw CatalogType because CatalogKey
// won't know how to
// deserialize it on the other side
Class<?> key_class = e.getKey().getClass();
if (key_class.equals(CatalogType.class))
throw new JSONException("CatalogType is not allowed to be the map key");
key_value = makePrimitiveValue(key_class, e.getKey()).toString();
}
stringer.key(key_value);
// We can also handle null values. Where is your god now???
if (e.getValue() == null) {
stringer.value(null);
} else {
writeFieldValue(stringer, e.getValue().getClass(), e.getValue());
}
} // FOR
stringer.endObject();
}
// Primitives
else {
if (debug.val)
LOG.debug("writePrimitiveFieldValue(" + field_class + ", " + field_value + ")");
stringer.value(makePrimitiveValue(field_class, field_value));
}
return;
}
/**
* Read data from the given JSONObject and populate the given Map
*
* @param json_object
* @param catalog_db
* @param map
* @param inner_classes
* @throws Exception
*/
@SuppressWarnings("unchecked")
protected static void readMapField(final JSONObject json_object, final Database catalog_db, final Map map, final Stack<Class<?>> inner_classes) throws Exception {
Class<?> key_class = inner_classes.pop();
Class<?> val_class = inner_classes.pop();
Collection<Class<?>> val_interfaces = ClassUtil.getInterfaces(val_class);
final Stack<Class<?>> next_inner_classes = new Stack<Class<?>>();
assert (json_object != null);
for (String json_key : CollectionUtil.iterable(json_object.keys())) {
next_inner_classes.clear();
next_inner_classes.addAll(inner_classes);
assert (next_inner_classes.equals(inner_classes));
// KEY
Object key = JSONUtil.getPrimitiveValue(json_key, key_class, catalog_db);
// VALUE
Object object = null;
if (json_object.isNull(json_key)) {
// Nothing...
} else if (val_interfaces.contains(List.class)) {
object = new ArrayList<Object>();
readCollectionField(json_object.getJSONArray(json_key), catalog_db, (Collection<?>) object, next_inner_classes);
} else if (val_interfaces.contains(Set.class)) {
object = new HashSet<Object>();
readCollectionField(json_object.getJSONArray(json_key), catalog_db, (Collection<?>) object, next_inner_classes);
} else if (val_interfaces.contains(Map.class)) {
object = new HashMap<Object, Object>();
readMapField(json_object.getJSONObject(json_key), catalog_db, (Map<?,?>) object, next_inner_classes);
} else {
String json_string = json_object.getString(json_key);
try {
object = JSONUtil.getPrimitiveValue(json_string, val_class, catalog_db);
} catch (Exception ex) {
System.err.println("val_interfaces: " + val_interfaces);
LOG.error("Failed to deserialize value '" + json_string + "' from inner map key '" + json_key + "'");
throw ex;
}
}
map.put(key, object);
}
}
/**
* Read data from the given JSONArray and populate the given Collection
*
* @param json_array
* @param catalog_db
* @param collection
* @param inner_classes
* @throws Exception
*/
@SuppressWarnings("unchecked")
protected static void readCollectionField(final JSONArray json_array, final Database catalog_db, final Collection collection, final Stack<Class<?>> inner_classes) throws Exception {
// We need to figure out what the inner type of the collection is
// If it's a Collection or a Map, then we need to instantiate it before
// we can call readFieldValue() again for it.
Class<?> inner_class = inner_classes.pop();
Collection<Class<?>> inner_interfaces = ClassUtil.getInterfaces(inner_class);
final Stack<Class<?>> next_inner_classes = new Stack<Class<?>>();
for (int i = 0, cnt = json_array.length(); i < cnt; i++) {
if (i > 0) next_inner_classes.clear();
next_inner_classes.addAll(inner_classes);
assert (next_inner_classes.equals(inner_classes));
Object value = null;
// Null
if (json_array.isNull(i)) {
value = null;
// Lists
} else if (inner_interfaces.contains(List.class)) {
value = new ArrayList<Object>();
readCollectionField(json_array.getJSONArray(i), catalog_db, (Collection<?>) value, next_inner_classes);
// Sets
} else if (inner_interfaces.contains(Set.class)) {
value = new HashSet<Object>();
readCollectionField(json_array.getJSONArray(i), catalog_db, (Collection<?>) value, next_inner_classes);
// Maps
} else if (inner_interfaces.contains(Map.class)) {
value = new HashMap<Object, Object>();
readMapField(json_array.getJSONObject(i), catalog_db, (Map<Object, Object>) value, next_inner_classes);
// Values
} else {
String json_string = json_array.getString(i);
value = JSONUtil.getPrimitiveValue(json_string, inner_class, catalog_db);
}
collection.add(value);
} // FOR
return;
}
/**
* @param json_object
* @param catalog_db
* @param json_key
* @param field_handle
* @param object
* @throws Exception
*/
public static void readFieldValue(final JSONObject json_object, final Database catalog_db, final String json_key, Field field_handle, Object object) throws Exception {
assert (json_object.has(json_key)) : "No entry exists for '" + json_key + "'";
Class<?> field_class = field_handle.getType();
Object field_object = field_handle.get(object);
// String field_name = field_handle.getName();
// Null
if (json_object.isNull(json_key)) {
if (debug.val)
LOG.debug("Field " + json_key + " is null");
field_handle.set(object, null);
// Collections
} else if (ClassUtil.getInterfaces(field_class).contains(Collection.class)) {
if (debug.val)
LOG.debug("Field " + json_key + " is a collection");
assert (field_object != null);
Stack<Class<?>> inner_classes = new Stack<Class<?>>();
inner_classes.addAll(ClassUtil.getGenericTypes(field_handle));
Collections.reverse(inner_classes);
JSONArray json_inner = json_object.getJSONArray(json_key);
if (json_inner == null)
throw new JSONException("No array exists for '" + json_key + "'");
readCollectionField(json_inner, catalog_db, (Collection) field_object, inner_classes);
// Maps
} else if (field_object instanceof Map) {
if (debug.val)
LOG.debug("Field " + json_key + " is a map");
assert (field_object != null);
Stack<Class<?>> inner_classes = new Stack<Class<?>>();
inner_classes.addAll(ClassUtil.getGenericTypes(field_handle));
Collections.reverse(inner_classes);
JSONObject json_inner = json_object.getJSONObject(json_key);
if (json_inner == null)
throw new JSONException("No object exists for '" + json_key + "'");
readMapField(json_inner, catalog_db, (Map) field_object, inner_classes);
// Everything else...
} else {
Class<?> explicit_field_class = JSONUtil.getClassForField(json_object, json_key);
if (explicit_field_class != null) {
field_class = explicit_field_class;
if (debug.val)
LOG.debug("Found explict field class " + field_class.getSimpleName() + " for " + json_key);
}
if (debug.val)
LOG.debug("Field " + json_key + " is primitive type " + field_class.getSimpleName());
Object value = JSONUtil.getPrimitiveValue(json_object.getString(json_key), field_class, catalog_db);
field_handle.set(object, value);
if (debug.val)
LOG.debug("Set field " + json_key + " to '" + value + "'");
}
}
/**
* For the given enum, load in the values from the JSON object into the
* current object This will throw errors if a field is missing
*
* @param <E>
* @param json_object
* @param catalog_db
* @param members
* @throws JSONException
*/
public static <E extends Enum<?>, T> void fieldsFromJSON(JSONObject json_object, Database catalog_db, T object, Class<? extends T> base_class, E... members) throws JSONException {
JSONUtil.fieldsFromJSON(json_object, catalog_db, object, base_class, false, members);
}
/**
* For the given enum, load in the values from the JSON object into the
* current object If ignore_missing is false, then JSONUtil will not throw
* an error if a field is missing
*
* @param <E>
* @param <T>
* @param json_object
* @param catalog_db
* @param object
* @param base_class
* @param ignore_missing
* @param members
* @throws JSONException
*/
public static <E extends Enum<?>, T> void fieldsFromJSON(JSONObject json_object, Database catalog_db, T object, Class<? extends T> base_class, boolean ignore_missing, E... members)
throws JSONException {
try {
fieldsFromJSON(json_object, catalog_db, object, base_class, ignore_missing, ClassUtil.getFieldsFromMembersEnum(base_class, members));
} catch (NoSuchFieldException ex) {
throw new JSONException(ex);
}
}
/**
* For the given list of Fields, load in the values from the JSON object
* into the current object.
* If ignore_missing is false, then JSONUtil will not throw an error if a field is missing
* @param <E>
* @param <T>
* @param json_object
* @param catalog_db
* @param object
* @param base_class
* @param ignore_missing
* @param fields
* @throws JSONException
*/
public static <E extends Enum<?>, T> void fieldsFromJSON(JSONObject json_object, Database catalog_db, T object, Class<? extends T> base_class, boolean ignore_missing, Field... fields)
throws JSONException {
for (Field field_handle : fields) {
String json_key = field_handle.getName().toUpperCase();
if (debug.val)
LOG.debug("Retreiving value for field '" + json_key + "'");
if (!json_object.has(json_key)) {
String msg = "JSONObject for " + base_class.getSimpleName() + " does not have key '" + json_key + "': " + CollectionUtil.list(json_object.keys());
if (ignore_missing) {
if (debug.val)
LOG.warn(msg);
continue;
} else {
throw new JSONException(msg);
}
}
try {
readFieldValue(json_object, catalog_db, json_key, field_handle, object);
} catch (Exception ex) {
// System.err.println(field_class + ": " +
// ClassUtil.getSuperClasses(field_class));
LOG.error("Unable to deserialize field '" + json_key + "' from " + base_class.getSimpleName(), ex);
throw new JSONException(ex);
}
} // FOR
}
/**
* For some fields we will also store the class of the value
*
* @param stringer
* @param json_key
* @param field_class
* @throws JSONException
*/
private static void addClassForField(JSONStringer stringer, String json_key, Class<?> field_class, Object field_value) throws JSONException {
// If the field_class is just a CatalogType, then store the true class
// of the object
// so that we can deserialize it on the other side
if (field_class.equals(CatalogType.class)) {
stringer.key(json_key + JSON_CLASS_SUFFIX).value(makePrimitiveValue(Class.class, field_value.getClass()));
}
}
/**
* Return the class of a field if it was stored in the JSONObject along with
* the value If there is no class information, then this will return null
*
* @param json_object
* @param json_key
* @return
* @throws JSONException
*/
private static Class<?> getClassForField(JSONObject json_object, String json_key) throws JSONException {
Class<?> field_class = null;
// Check whether we also stored the class
if (json_object.has(json_key + JSON_CLASS_SUFFIX)) {
try {
field_class = ClassUtil.getClass(json_object.getString(json_key + JSON_CLASS_SUFFIX));
} catch (Exception ex) {
LOG.error("Failed to include class for field '" + json_key + "'", ex);
throw new JSONException(ex);
}
}
return (field_class);
}
/**
* Return the proper serialization string for the given value
*
* @param field_name
* @param field_class
* @param field_value
* @return
*/
private static Object makePrimitiveValue(Class<?> field_class, Object field_value) {
Object value = null;
// Class
if (field_class.equals(Class.class)) {
value = ((Class<?>) field_value).getName();
// JSONSerializable
} else if (ClassUtil.getInterfaces(field_class).contains(JSONSerializable.class)) {
// Just return the value back. The JSON library will take care of it
// System.err.println(field_class + ": " + field_value);
value = field_value;
// VoltDB Catalog
} else if (ClassUtil.getSuperClasses(field_class).contains(CatalogType.class)) {
value = CatalogKey.createKey((CatalogType) field_value);
// Everything else
} else {
value = field_value; // .toString();
}
return (value);
}
/**
* For the given JSON string, figure out what kind of object it is and
* return it
*
* @param json_value
* @param field_class
* @param catalog_db
* @return
* @throws Exception
*/
private static Object getPrimitiveValue(String json_value, Class<?> field_class, Database catalog_db) throws Exception {
Object value = null;
// VoltDB Catalog Object
if (ClassUtil.getSuperClasses(field_class).contains(CatalogType.class)) {
@SuppressWarnings("unchecked")
Class<? extends CatalogType> catalog_class = (Class<? extends CatalogType>)field_class;
try {
value = CatalogKey.getFromKey(catalog_db, json_value, catalog_class);
} catch (Throwable ex) {
throw new Exception("Failed to get catalog object from \"" + json_value + "\"", ex);
}
if (value == null)
throw new JSONException("Failed to get catalog object from \"" + json_value + "\"");
}
// Class
else if (field_class.equals(Class.class)) {
value = ClassUtil.getClass(json_value);
if (value == null)
throw new JSONException("Failed to get class from '" + json_value + "'");
}
// Enum
else if (field_class.isEnum()) {
if (field_class.equals(VoltType.class)) {
json_value = json_value.replace("VoltType.", "");
}
for (Object o : field_class.getEnumConstants()) {
Enum<?> e = (Enum<?>) o;
if (json_value.equals(e.name()))
return (e);
} // FOR
throw new JSONException("Invalid enum value '" + json_value + "': " + Arrays.toString(field_class.getEnumConstants()));
}
// Boolean
else if (field_class.equals(Boolean.class) || field_class.equals(boolean.class)) {
// We have to use field_class.equals() because the value may be null
value = Boolean.parseBoolean(json_value);
}
// Integer
else if (field_class.equals(Integer.class) || field_class.equals(int.class)) {
value = Integer.parseInt(json_value);
}
// Float
else if (field_class.equals(Float.class) || field_class.equals(float.class)) {
value = Float.parseFloat(json_value);
}
// JSONSerializable
else if (ClassUtil.getInterfaces(field_class).contains(JSONSerializable.class)) {
value = ClassUtil.newInstance(field_class, null, null);
((JSONSerializable) value).fromJSON(new JSONObject(json_value), catalog_db);
}
// Everything else
else {
// LOG.debug(json_value + " -> " + field_class);
VoltType volt_type = VoltType.typeFromClass(field_class);
value = VoltTypeUtil.getObjectFromString(volt_type, json_value);
}
return (value);
}
}