package triaina.commons.json; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import triaina.commons.collection.ImmutableHashMap; import triaina.commons.exception.CommonRuntimeException; import triaina.commons.exception.JSONConvertException; import triaina.commons.exception.JSONRuntimeException; import triaina.commons.json.annotation.Exclude; import triaina.commons.utils.FieldUtils; import triaina.commons.utils.JSONArrayUtils; import triaina.commons.utils.JSONObjectUtils; import triaina.commons.utils.NamingConventionUtils; import android.os.Bundle; import android.util.Log; /** * - Note * The field convert name convention example is following. * mSomeField <-> some_field * someField <-> some_field * some_field <-> some_field * * @author hnakagawa * @author Guillaume Legrand * */ public final class JSONConverter { private static final String TAG = JSONConverter.class.getCanonicalName(); private JSONConverter() {} private static ConcurrentMap<Class<?>, ImmutableHashMap<String, Field>> mBindCache = new ConcurrentHashMap<Class<?>, ImmutableHashMap<String,Field>>(); private static ConcurrentMap<Class<?>, ImmutableHashMap<Field, String>> mPropCache = new ConcurrentHashMap<Class<?>, ImmutableHashMap<Field, String>>(); /** * Class to convert JSON data to or from Java "primitive" types (including Java primitive boxing types - e.g. Integer - and Strings). * It basically converts everything that doesn't translate into a JSONObject or JSONArray or NULL. * * This is not really necessary for types supported by the JSON library (String, Integer, Double...) especially thanks to autoboxing, * however without this class other types like float wouldn't be supported as we can't cast unrelated classes like the different numerical types. */ private static abstract class PrimitiveConverter <T> { public abstract T retrieve (JSONObject json, String key) throws JSONException; public abstract T retrieve (JSONArray json, int index) throws JSONException; /** * Converts a T to an Object of a type which can be put() in a JSON object or array. */ protected Object convertToPutableObject (T value) { return value; //default, override for types non-supported by JSON classes } public void put (final JSONObject json, final String key, final T value) throws JSONException { json.put(key, convertToPutableObject(value)); } public void put (final JSONArray json, final int index, final T value) throws JSONException { json.put(index, convertToPutableObject(value)); } public void put (final JSONArray json, final T value) throws JSONException { json.put(convertToPutableObject(value)); } } private static final ImmutableHashMap<Class<?>, PrimitiveConverter<?>> PRIMITIVE_CONVERTERS; static { HashMap<Class<?>, PrimitiveConverter<?>> map = new HashMap<Class<?>, PrimitiveConverter<?>>(); final PrimitiveConverter<String> jsonStringRetriever = new PrimitiveConverter<String> () { @Override public String retrieve (final JSONObject json, final String key) throws JSONException { return json.getString(key); } @Override public String retrieve(final JSONArray json, final int index) throws JSONException { return json.getString(index); } }; final PrimitiveConverter<Boolean> jsonBooleanRetriever = new PrimitiveConverter<Boolean> () { @Override public Boolean retrieve (final JSONObject json, final String key) throws JSONException { return json.getBoolean(key); } @Override public Boolean retrieve(final JSONArray json, final int index) throws JSONException { return json.getBoolean(index); } }; final PrimitiveConverter<Integer> jsonIntegerRetriever = new PrimitiveConverter<Integer> () { @Override public Integer retrieve (final JSONObject json, final String key) throws JSONException { return json.getInt(key); } @Override public Integer retrieve(final JSONArray json, final int index) throws JSONException { return json.getInt(index); } }; final PrimitiveConverter<Long> jsonLongRetriever = new PrimitiveConverter<Long> () { @Override public Long retrieve (final JSONObject json, final String key) throws JSONException { return json.getLong(key); } @Override public Long retrieve(final JSONArray json, final int index) throws JSONException { return json.getLong(index); } }; final PrimitiveConverter<Double> jsonDoubleRetriever = new PrimitiveConverter<Double> () { @Override public Double retrieve (final JSONObject json, final String key) throws JSONException { return json.getDouble(key); } @Override public Double retrieve(final JSONArray json, final int index) throws JSONException { return json.getDouble(index); } }; final PrimitiveConverter<Float> jsonFloatRetriever = new PrimitiveConverter<Float> () { @Override public Float retrieve (final JSONObject json, final String key) throws JSONException { return ((Double)json.getDouble(key)).floatValue(); } @Override public Float retrieve(final JSONArray json, final int index) throws JSONException { return ((Double)json.getDouble(index)).floatValue(); } @Override protected Double convertToPutableObject (final Float value) { return value.doubleValue(); } }; final PrimitiveConverter<Short> jsonShortRetriever = new PrimitiveConverter<Short> () { @Override public Short retrieve (final JSONObject json, final String key) throws JSONException { return ((Integer)json.getInt(key)).shortValue(); } @Override public Short retrieve (final JSONArray json, final int index) throws JSONException { return ((Integer)json.getInt(index)).shortValue(); } @Override protected Integer convertToPutableObject (final Short value) { return value.intValue(); } }; final PrimitiveConverter<Byte> jsonByteRetriever = new PrimitiveConverter<Byte> () { @Override public Byte retrieve (final JSONObject json, final String key) throws JSONException { return ((Integer)json.getInt(key)).byteValue(); } @Override public Byte retrieve (final JSONArray json, final int index) throws JSONException { return ((Integer)json.getInt(index)).byteValue(); } @Override protected Integer convertToPutableObject (final Byte value) { return value.intValue(); } }; final PrimitiveConverter<Character> jsonCharRetriever = new PrimitiveConverter<Character> () { @Override public Character retrieve (final JSONObject json, final String key) throws JSONException { return (json.getString(key)).charAt(0); } @Override public Character retrieve (final JSONArray json, final int index) throws JSONException { return (json.getString(index)).charAt(0); } @Override protected String convertToPutableObject (final Character value) { return value.toString(); } }; map.put(String.class , jsonStringRetriever); map.put(Boolean.class , jsonBooleanRetriever); map.put(boolean.class , jsonBooleanRetriever); map.put(Integer.class , jsonIntegerRetriever); map.put(int.class , jsonIntegerRetriever); map.put(Long.class , jsonLongRetriever); map.put(long.class , jsonLongRetriever); map.put(Double.class , jsonDoubleRetriever); map.put(double.class , jsonDoubleRetriever); map.put(Float.class , jsonFloatRetriever); map.put(float.class , jsonFloatRetriever); map.put(Short.class , jsonShortRetriever); map.put(short.class , jsonShortRetriever); map.put(Byte.class , jsonByteRetriever); map.put(byte.class , jsonByteRetriever); map.put(Character.class, jsonCharRetriever); map.put(char.class , jsonCharRetriever); PRIMITIVE_CONVERTERS = new ImmutableHashMap<Class<?>, PrimitiveConverter<?>>(map); } private static PrimitiveConverter<?> getPrimitiveConverter (final Class<?> typeToConvertTo) { return PRIMITIVE_CONVERTERS.get(typeToConvertTo); } private static boolean isPrimitivelyConvertible (final Class<?> type) { return PRIMITIVE_CONVERTERS.containsKey(type); } @SuppressWarnings("unchecked") private static Object retrieve (final Class<?> type, final JSONObject json, final String key, final Object enclosingObject) throws JSONConvertException { if (json.isNull(key)) return null; try { if (isPrimitivelyConvertible(type)) { return getPrimitiveConverter(type).retrieve(json, key); } else if (type.isArray()) { return toObjectArray(json.getJSONArray(key), type.getComponentType(), enclosingObject); } else if (type.isEnum()) { @SuppressWarnings("rawtypes") final Class<? extends Enum> enumType = type.asSubclass(Enum.class); final String enumValue = NamingConventionUtils.fromJsonNameToJavaEnumName(json.getString(key)); try { return Enum.valueOf(enumType, enumValue); } catch (final IllegalArgumentException exp) { throw new JSONConvertException(exp); } } else if (Bundle.class.equals(type)) { final Bundle bundle = new Bundle(); bind(json.getJSONObject(key), bundle); return bundle; } else { return toObject(json.getJSONObject(key), type, enclosingObject); } }catch (final JSONException exp) { throw new JSONConvertException(exp); } } @SuppressWarnings("unchecked") private static Object retrieve (final Class<?> type, final JSONArray json, final int index, final Object enclosingObject) throws JSONConvertException { if (json.isNull(index)) return null; try { if (isPrimitivelyConvertible(type)) { return getPrimitiveConverter(type).retrieve(json, index); } else if (type.isArray()) { return toObjectArray(json.getJSONArray(index), type.getComponentType(), enclosingObject); } else if (type.isEnum()) { @SuppressWarnings("rawtypes") final Class<? extends Enum> enumType = type.asSubclass(Enum.class); final String enumValue = NamingConventionUtils.fromJsonNameToJavaEnumName(json.getString(index)); try { return Enum.valueOf(enumType, enumValue); } catch (final IllegalArgumentException exp) { throw new JSONConvertException(exp); } } else if (Bundle.class.equals(type)) { final Bundle bundle = new Bundle(); bind(json.getJSONObject(index), bundle); return bundle; } else { return toObject(json.getJSONObject(index), type, enclosingObject); } }catch (final JSONException exp) { throw new JSONConvertException(exp); } } @SuppressWarnings("unchecked") //cf. comment below private static void put (final JSONObject json, final String key, final Object value) throws JSONConvertException { if (value == null) { //just put it in the destination array : try { json.put(key, value); } catch (final JSONException exp) { throw new JSONConvertException(exp); } } else { final Class<?> type = value.getClass(); try { if (isPrimitivelyConvertible(type)) { @SuppressWarnings("rawtypes") //this is on purpose ; we need to call one of converter's generic-parameterized method, so as we don't know the type of 'value' at compile time, we need to explicitly call its bridge method final PrimitiveConverter converter = getPrimitiveConverter(type); converter.put(json, key, value); } else { final Object valueToPut; if (type.isArray()) { valueToPut = toJSONArray((Object[])value); } //TODO Enum else if (Bundle.class.equals(type)) { valueToPut = toJSON((Bundle)value); } else { valueToPut = toJSON(value); } json.put(key, valueToPut); } } catch (final JSONException exp) { throw new JSONConvertException(exp); } } } @SuppressWarnings({ "unchecked", "unused" }) //cf. comment below for unchecked //not currently used but keep as utility method private static void put (final JSONArray json, final int index, final Object value) throws JSONConvertException { if (value == null) { //just put it in the destination array : try { json.put(index, value); } catch (final JSONException exp) { throw new JSONConvertException(exp); } } else { final Class<?> type = value.getClass(); try { if (isPrimitivelyConvertible(type)) { @SuppressWarnings("rawtypes") //this is on purpose ; we need to call one of converter's generic-parameterized method, so as we don't know the type of 'value' at compile time, we need to explicitly call its bridge method final PrimitiveConverter converter = getPrimitiveConverter(type); converter.put(json, index, value); } else { final Object valueToPut; if (type.isArray()) { valueToPut = toJSONArray((Object[])value); } //TODO enum else if (Bundle.class.equals(type)) { valueToPut = toJSON((Bundle)value); } else { valueToPut = toJSON(value); } json.put(index, valueToPut); } } catch (final JSONException exp) { throw new JSONConvertException(exp); } } } @SuppressWarnings("unchecked") //cf. comment below private static void put (final JSONArray json, final Object value) throws JSONConvertException { if (value == null) { //just put it in the destination array : json.put(value); } else { final Class<?> type = value.getClass(); try { if (isPrimitivelyConvertible(type)) { @SuppressWarnings("rawtypes") //this is on purpose ; we need to call one of converter's generic-parameterized method, so as we don't know the type of 'value' at compile time, we need to explicitly call its bridge method final PrimitiveConverter converter = getPrimitiveConverter(type); converter.put(json, value); } else { final Object valueToPut; if (type.isArray()) { valueToPut = toJSONArray((Object[])value); } //TODO enum else if (Bundle.class.equals(type)) { valueToPut = toJSON((Bundle)value); } else { valueToPut = toJSON(value); } json.put(valueToPut); } } catch (final JSONException exp) { throw new JSONConvertException(exp); } } } public static <T> T toObjectNoException(String jsonText, Class<T> clazz) { try { return toObjectNoException(JSONObjectUtils.parse(jsonText), clazz); } catch (JSONRuntimeException exp) { Log.e(TAG, exp.getMessage() + "", exp); } return null; } public static <T> T toObject(String jsonText, Class<T> clazz) throws JSONConvertException { try { return toObject(new JSONObject(jsonText), clazz); } catch (JSONException exp) { throw new JSONConvertException(exp); } } public static <T> T toObjectNoException(JSONObject json, Class<T> clazz) { try { return JSONConverter.toObject(json, clazz); } catch (JSONConvertException exp) { Log.e(TAG, exp.getMessage() + "", exp); } //if an exception occurred : return null; } public static <T> T toObject (final JSONObject json, final Class<T> type) throws JSONConvertException { return toObject(json, type, null); } private static <T> T toObject (final JSONObject json, final Class<T> type, final Object enclosingInstance) throws JSONConvertException { try { final T obj; if (type.isMemberClass() && ((type.getModifiers() & Modifier.STATIC) != Modifier.STATIC)) { //if non-static member (aka inner) class (not provided by reflection API) //need to supply the enclosing instance final Constructor<T> defaultConstructor = type.getConstructor(type.getEnclosingClass()); obj = defaultConstructor.newInstance(enclosingInstance); } else { obj = type.newInstance(); } bind(json, obj); return obj; } catch (CommonRuntimeException exp) { throw new JSONConvertException(exp); } catch (IllegalAccessException exp) { throw new JSONConvertException(exp); } catch (InstantiationException exp) { throw new JSONConvertException(exp); } catch (final NoSuchMethodException exp) { throw new JSONConvertException(exp); } catch (final InvocationTargetException exp) { throw new JSONConvertException(exp); } } public static <T> T[] toObjectArrayNoException (final JSONArray json, Class<T> clazz) { try { return toObjectArray(json, clazz); } catch (final JSONConvertException exp) { Log.e(TAG, exp.getMessage() + "", exp); } //if an exception occurred : return null; } public static <T> T[] toObjectArray (final JSONArray json, Class<T> clazz) throws JSONConvertException { return toObjectArray(json, clazz, null); } private static <T> T[] toObjectArray (final JSONArray json, Class<T> clazz, Object enclosingObject) throws JSONConvertException { @SuppressWarnings("unchecked") final T[] objectArray = (T[])Array.newInstance(clazz, json.length()); try { bind(json, objectArray, enclosingObject); } catch (final IllegalAccessException exp) { throw new JSONConvertException(exp); } catch (final InstantiationException exp) { throw new JSONConvertException(exp); } return objectArray; } private static void bind(JSONObject json, Bundle bundle) { for (@SuppressWarnings("rawtypes") Iterator iterator = json.keys(); iterator.hasNext();) { String key = (String) iterator.next(); JSONObject child = json.optJSONObject(key); if (child != null) { Bundle value = new Bundle(); bind(child, value); bundle.putBundle(key, value); continue; } JSONArray arr = json.optJSONArray(key); if (arr != null) { String[] values = JSONArrayUtils.toStringArray(arr); bundle.putStringArray(key, values); continue; } String value = json.optString(key); bundle.putString(key, value); } } private static void bind (final JSONObject json, final Object obj) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, JSONConvertException { final ImmutableHashMap<String, Field> fieldMap = loadBindMap(obj.getClass()); final Set<String> keySet = fieldMap.keySet(); for (String key : keySet) { final Field field = fieldMap.get(key); final Class<?> type = field.getType(); final Object value = retrieve(type, json, key, obj); if (value != null) //if null, we just let the Java object to its default value (or the one set in the constructor) FieldUtils.setNoException(obj, field, value); } } private static void bind (final JSONArray jsonArr, final Object[] objs, final Object enclosingObject) throws IllegalAccessException, InstantiationException, JSONConvertException { final Class<?> type = objs.getClass(); final Class<?> compType = type.getComponentType(); for (int i = 0; i < objs.length; i++) { final Object obj = retrieve(compType, jsonArr, i, enclosingObject); if (obj != null) //if null, leave to default value objs[i] = obj; } } public static JSONObject toJSONNoException(Object obj) { try { return toJSON(obj); } catch (Exception exp) { Log.e(TAG, exp.getMessage() + "", exp); } return null; } public static JSONObject toJSON(Bundle bundle) throws JSONConvertException { if (bundle == null) { return null; } JSONObject json = new JSONObject(); Set<String> keySet = bundle.keySet(); for (String key : keySet) { put(json, key, bundle.get(key)); } return json; } public static JSONObject toJSON (final Object obj) throws JSONConvertException { if (obj == null) return null; final JSONObject json = new JSONObject(); final ImmutableHashMap<Field, String> propMap = loadPropMap(obj.getClass()); final Set<Field> fieldSet = propMap.keySet(); for (final Field field : fieldSet) { put(json, propMap.get(field), FieldUtils.get(obj, field)); } return json; } public static JSONArray toJSONArray (final Object[] objs) throws JSONConvertException { JSONArray jsonArr = null; if (objs != null) { jsonArr = new JSONArray(); for (Object obj : objs) { put(jsonArr, obj); } } return jsonArr; } private static ImmutableHashMap<String, Field> loadBindMap(Class<?> clazz) { ImmutableHashMap<String, Field> bindMap = mBindCache.get(clazz); if (bindMap != null) { return bindMap; } bindMap = createBindMap(clazz); mBindCache.put(clazz, bindMap); return bindMap; } private static ImmutableHashMap<String, Field> createBindMap(Class<?> clazz) { HashMap<String, Field> bindMap = new HashMap<String, Field>(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { Exclude exc = field.getAnnotation(Exclude.class); if (exc == null) { field.setAccessible(true); String name = NamingConventionUtils.fromJavaFieldNameToJSONName(field.getName()); bindMap.put(name, field); } } return new ImmutableHashMap<String, Field>(bindMap); } private static ImmutableHashMap<Field, String> loadPropMap(Class<?> clazz) { ImmutableHashMap<Field, String> propMap = mPropCache.get(clazz); if (propMap != null) { return propMap; } propMap = createPropMap(clazz); mPropCache.put(clazz, propMap); return propMap; } private static ImmutableHashMap<Field, String> createPropMap(Class<?> clazz) { HashMap<Field, String> propMap = new HashMap<Field, String>(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (!Modifier.isStatic(field.getModifiers()) && !field.isAnnotationPresent(Exclude.class) && !field.isSynthetic()) { //ignore Exclude annotated fields and synthetic fields field.setAccessible(true); String name = NamingConventionUtils.fromJavaFieldNameToJSONName(field.getName()); propMap.put(field, name); } } return new ImmutableHashMap<Field, String>(propMap); } }