package ee.telekom.workflow.util; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.internal.LinkedTreeMap; /** * Utility that serialises and deserialises any deep object tree without cyclic references. * <p> * Common JSON libraries such as jackson or gson all support to serialise such deep object trees. * However, none of the known libraries supports to to deserialise json strings that describe * Object[] or Map<String,Object> fields or classes that contain fields of collections. * <p> * The general problem is that the class information of a given object instance is lost during * serialisation. E.g. it is not possible to deserialise the object array * <pre>new SportMatch[] {new FootballMatch("Arsenal","Chelsea"), new TennisMatch("Nadal","Djokovic")}</pre> * based on the json representation * <pre>[{oponent1:"Arsenal", oponent2:"Chelsea"},{oponent1:"Nadal", oponent2:"Djokovic"}]</pre> * * This library overcomes this issue and also serialises class information where the class type * cannot be deducted otherwise. When serialising it allows to choose whether to serialise class * type information for the root object. Not serialising root object class type information is * appropriate if you can predict the root object class type at deserialization time. E.g. * <pre> * // if you can predict the type * String json = JsonUtil.serialize(new SportMatch[] {new FootballMatch("Arsenal","Chelsea"), new TennisMatch("Nadal","Djokovic")},false); * SportMatch[] o = JsonUtil.deserialize(json, SportMatch[].class); * * // if you cannot predict the type * String json = JsonUtil.serialize(new SportMatch[] {new FootballMatch("Arsenal","Chelsea"), new TennisMatch("Nadal","Djokovic")},true); * Object o = JsonUtil.deserialize(json); * </pre> */ public class JsonUtil{ private static final Gson gson = new GsonBuilder().serializeNulls().create(); private static final String ARRAYS_ASLIST_CLASSNAME = Arrays.asList( "" ).getClass().getName(); private static final String SINGLETON_LIST_CLASSNAME = Collections.singletonList( "" ).getClass().getName(); private static final String SINGLETON_SET_CLASSNAME = Collections.singleton( "" ).getClass().getName(); private static final String SINGLETON_MAP_CLASSNAME = Collections.singletonMap( "", "" ).getClass().getName(); private static final Class<?> ARRAYS_ASLIST_CLASS = Arrays.asList( "" ).getClass(); private static final Class<?> SINGLETON_LIST_CLASS = Collections.singletonList( "" ).getClass(); private static final Class<?> SINGLETON_SET_CLASS = Collections.singleton( "" ).getClass(); private static final Class<?> SINGLETON_MAP_CLASS = Collections.singletonMap( "", "" ).getClass(); public static String serialize( boolean object ){ return gson.toJson( convert( object, boolean.class, false ) ); } public static String serialize( byte object ){ return gson.toJson( convert( object, byte.class, false ) ); } public static String serialize( short object ){ return gson.toJson( convert( object, short.class, false ) ); } public static String serialize( int object ){ return gson.toJson( convert( object, int.class, false ) ); } public static String serialize( long object ){ return gson.toJson( convert( object, long.class, false ) ); } public static String serialize( float object ){ return gson.toJson( convert( object, float.class, false ) ); } public static String serialize( double object ){ return gson.toJson( convert( object, double.class, false ) ); } public static String serialize( Object object, boolean serializeType ){ if( object == null ){ return null; } Class<?> type = object.getClass(); return gson.toJson( convert( object, type, serializeType ) ); } public static String serializeCollection( Collection<?> object, boolean serializeType, boolean serializeElementType ){ if( object == null ){ return null; } Class<?> type = object.getClass(); return gson.toJson( convertCollection( object, type, serializeType, serializeElementType ) ); } public static boolean deserializeBoolean( String json ){ return gson.fromJson( json, boolean.class ); } public static byte deserializeByte( String json ){ return gson.fromJson( json, byte.class ); } public static short deserializeShort( String json ){ return gson.fromJson( json, short.class ); } public static int deserializeInt( String json ){ return gson.fromJson( json, int.class ); } public static long deserializeLong( String json ){ return gson.fromJson( json, long.class ); } public static float deserializeFloat( String json ){ return gson.fromJson( json, float.class ); } public static double deserializeDouble( String json ){ return gson.fromJson( json, double.class ); } public static Object deserialize( String json ){ if( json == null ){ return null; } return deserialize( json, null ); } public static <T> T deserialize( String json, Class<T> type ){ if( json == null ){ return null; } JsonParser parser = new JsonParser(); JsonElement element = parser.parse( json ); @SuppressWarnings("unchecked") T result = (T)deconvert( element, type ); return result; } public static <K, V> HashMap<K, V> deserializeHashMap( String json, Class<K> key, Class<V> value ){ @SuppressWarnings("unchecked") HashMap<K, V> result = deserialize( json, HashMap.class ); return result; } public static <T> Collection<T> deserializeCollection( String json, @SuppressWarnings("rawtypes") Class<? extends Collection> type, Class<T> elementType ){ if( json == null ){ return null; } JsonParser parser = new JsonParser(); JsonElement element = parser.parse( json ); @SuppressWarnings("unchecked") Collection<T> result = (Collection<T>)deconvertCollection( element, type, elementType ); return result; } private static JsonElement convert( Object object, Class<?> type, boolean serializeType ){ if( object == null ){ return JsonNull.INSTANCE; } else if( isSimple( object.getClass() ) ){ return convertSimple( object, type, serializeType ); } else if( isArray( object.getClass() ) ){ return convertArray( object, type, serializeType ); } else if( isCollection( object.getClass() ) ){ return convertCollection( object, type, serializeType, true ); } else if( isMap( object.getClass() ) ){ return convertMap( object, type, serializeType ); } else{ return convertObject( object, type, serializeType ); } } private static Object deconvert( JsonElement element, Class<?> expectedType ){ if( element.isJsonNull() ){ return null; } else if( element.isJsonPrimitive() ){ return deconvertSimple( element, expectedType ); } Class<?> objectType; JsonElement objectElement; if( expectedType == null || isContainer( element ) ){ JsonObject container = element.getAsJsonObject(); String typeName = container.get( "c" ).getAsString(); objectType = getClass( typeName ); objectElement = container.get( "v" ); } else{ objectType = expectedType; objectElement = element; } if( isSimple( objectType ) ){ return deconvertSimple( objectElement, objectType ); } else if( isArray( objectType ) ){ return deconvertArray( objectElement, objectType ); } else if( isCollection( objectType ) ){ return deconvertCollection( objectElement, objectType, null ); } else if( isMap( objectType ) ){ return deconvertMap( objectElement, objectType ); } else{ return deconvertObject( objectElement, objectType ); } } private static boolean isSimple( Class<?> type ){ return Boolean.class.isAssignableFrom( type ) || Number.class.isAssignableFrom( type ) || String.class.isAssignableFrom( type ) || Date.class.isAssignableFrom( type ) || type.isEnum(); } private static JsonElement convertSimple( Object object, Class<?> type, boolean serializeType ){ JsonPrimitive primitive = null; if( object instanceof Boolean ){ primitive = new JsonPrimitive( (Boolean)object ); } else if( object instanceof Number ){ primitive = new JsonPrimitive( (Number)object ); } else if( object instanceof String ){ primitive = new JsonPrimitive( (String)object ); } else if( object instanceof Date ){ primitive = new JsonPrimitive( formatDate( (Date)object ) ); } else if( object.getClass().isEnum() ){ primitive = new JsonPrimitive( ((Enum<?>)object).name() ); } if( serializeType && !(object instanceof Boolean) && !(object instanceof String) ){ JsonObject container = new JsonObject(); container.add( "c", new JsonPrimitive( getTypeName( type ) ) ); container.add( "v", primitive ); return container; } else{ return primitive; } } private static Object deconvertSimple( JsonElement element, Class<?> type ){ String value = element.getAsString(); if( Boolean.class.equals( type ) || boolean.class.equals( type ) || element.getAsJsonPrimitive().isBoolean() ){ return Boolean.valueOf( value ); } else if( Byte.class.equals( type ) || byte.class.equals( type ) ){ return Byte.valueOf( value ); } else if( Short.class.equals( type ) || short.class.equals( type ) ){ return Short.valueOf( value ); } else if( Integer.class.equals( type ) || int.class.equals( type ) ){ return Integer.valueOf( value ); } else if( Long.class.equals( type ) || long.class.equals( type ) ){ return Long.valueOf( value ); } else if( Float.class.equals( type ) || float.class.equals( type ) ){ return Float.valueOf( value ); } else if( Double.class.equals( type ) || double.class.equals( type ) ){ return Double.valueOf( value ); } else if( BigInteger.class.equals( type ) ){ return new BigInteger( value ); } else if( BigDecimal.class.equals( type ) ){ return new BigDecimal( value ); } else if( Date.class.equals( type ) ){ return parseDate( value ); } else if( java.sql.Date.class.equals( type ) ){ return new java.sql.Date( parseDate(value).getTime() ); } else if( java.sql.Time.class.equals( type ) ){ return new java.sql.Time( parseDate(value).getTime() ); } else if( java.sql.Timestamp.class.equals( type ) ){ return new java.sql.Timestamp( parseDate(value).getTime() ); } else if( type != null && type.isEnum() ){ @SuppressWarnings({"unchecked", "rawtypes"}) Class<Enum> enumClazz = (Class<Enum>)type; @SuppressWarnings("unchecked") Object result = Enum.valueOf( enumClazz, value ); return result; } else if( String.class.equals( type ) || element.getAsJsonPrimitive().isString() ){ return value; } throw new RuntimeException( "Unable to deconvert element " + element ); } private static boolean isArray( Class<?> type ){ return type.isArray(); } private static JsonElement convertArray( Object object, Class<?> type, boolean serializeType ){ JsonArray array = new JsonArray(); Class<?> componentType = type.getComponentType(); if( boolean.class.equals( componentType ) ){ for( boolean element : (boolean[])object ){ array.add( convert( element, componentType, false ) ); } } else if( byte.class.equals( componentType ) ){ for( byte element : (byte[])object ){ array.add( convert( element, componentType, false ) ); } } else if( short.class.equals( componentType ) ){ for( short element : (short[])object ){ array.add( convert( element, componentType, false ) ); } } else if( int.class.equals( componentType ) ){ for( int element : (int[])object ){ array.add( convert( element, componentType, false ) ); } } else if( long.class.equals( componentType ) ){ for( long element : (long[])object ){ array.add( convert( element, componentType, false ) ); } } else if( float.class.equals( componentType ) ){ for( float element : (float[])object ){ array.add( convert( element, componentType, false ) ); } } else if( double.class.equals( componentType ) ){ for( double element : (double[])object ){ array.add( convert( element, componentType, false ) ); } } else{ Class<?> innermostComponentType = getInnermostComponentType( type ); boolean serializeElementType = !innermostComponentType.isPrimitive(); for( Object element : (Object[])object ){ array.add( convert( element, element == null ? null : element.getClass(), serializeElementType ) ); } } if( serializeType ){ JsonObject container = new JsonObject(); container.add( "c", new JsonPrimitive( getTypeName( type ) ) ); container.add( "v", array ); return container; } else{ return array; } } private static Object deconvertArray( JsonElement element, Class<?> type ){ JsonArray array = element.getAsJsonArray(); Class<?> componentType = type.getComponentType(); if( boolean.class.equals( componentType ) ){ boolean[] result = new boolean[array.size()]; for( int i = 0; i < array.size(); i++ ){ result[i] = array.get( i ).getAsBoolean(); } return result; } else if( byte.class.equals( componentType ) ){ byte[] result = new byte[array.size()]; for( int i = 0; i < array.size(); i++ ){ result[i] = array.get( i ).getAsByte(); } return result; } else if( short.class.equals( componentType ) ){ short[] result = new short[array.size()]; for( int i = 0; i < array.size(); i++ ){ result[i] = array.get( i ).getAsShort(); } return result; } else if( int.class.equals( componentType ) ){ int[] result = new int[array.size()]; for( int i = 0; i < array.size(); i++ ){ result[i] = array.get( i ).getAsInt(); } return result; } else if( long.class.equals( componentType ) ){ long[] result = new long[array.size()]; for( int i = 0; i < array.size(); i++ ){ result[i] = array.get( i ).getAsLong(); } return result; } else if( float.class.equals( componentType ) ){ float[] result = new float[array.size()]; for( int i = 0; i < array.size(); i++ ){ result[i] = array.get( i ).getAsFloat(); } return result; } else if( double.class.equals( componentType ) ){ double[] result = new double[array.size()]; for( int i = 0; i < array.size(); i++ ){ result[i] = array.get( i ).getAsDouble(); } return result; } else{ Object resultObject = Array.newInstance( componentType, array.size() ); Object[] result = (Object[])resultObject; for( int i = 0; i < array.size(); i++ ){ Object arrayElement = deconvert( array.get( i ), componentType ); result[i] = arrayElement; } return result; } } private static boolean isCollection( Class<?> type ){ return List.class.isAssignableFrom( type ) || Set.class.isAssignableFrom( type ); } private static JsonElement convertCollection( Object object, Class<?> type, boolean serializeType, boolean serializeElementType ){ JsonArray array = new JsonArray(); for( Object element : (Collection<?>)object ){ array.add( convert( element, element == null ? null : element.getClass(), serializeElementType ) ); } if( serializeType ){ JsonObject container = new JsonObject(); container.add( "c", new JsonPrimitive( getTypeName( type ) ) ); container.add( "v", array ); return container; } else{ return array; } } private static Collection<?> deconvertCollection( JsonElement element, Class<?> type, Class<?> elementType ){ JsonArray array = element.getAsJsonArray(); if( ARRAYS_ASLIST_CLASS.equals( type ) ){ Object[] values = new Object[array.size()]; for( int i = 0; i < array.size(); i++ ){ Object arrayElement = deconvert( array.get( i ), elementType ); values[i] = arrayElement; } return Arrays.asList( values ); } else if( SINGLETON_LIST_CLASS.equals( type ) ){ return Collections.singletonList( deconvert( array.get( 0 ), elementType ) ); } else if( SINGLETON_SET_CLASS.equals( type ) ){ return Collections.singleton( deconvert( array.get( 0 ), elementType ) ); } else{ @SuppressWarnings("unchecked") Collection<Object> collection = (Collection<Object>)instantiate( type ); for( int i = 0; i < array.size(); i++ ){ collection.add( deconvert( array.get( i ), elementType ) ); } return collection; } } private static boolean isMap( Class<?> type ){ return Map.class.isAssignableFrom( type ); } private static JsonElement convertMap( Object object, Class<?> type, boolean serializeType ){ Set<?> keySet = ((Map<?, ?>)object).keySet(); return isSetOfStrings( keySet ) ? convertStringKeyMap( object, type, serializeType ) : convertObjectKeyMap( object, type, serializeType ); } private static Map<?, ?> deconvertMap( JsonElement element, Class<?> type ){ if (type.equals(Map.class)){ // default implementation if type points to the interface type = HashMap.class; } return element.isJsonObject() ? deconvertStringKeyMap( element, type ) : deconvertObjectKeyMap( element, type ); } private static JsonElement convertObjectKeyMap( Object object, Class<?> type, boolean serializeType ){ JsonArray array = new JsonArray(); for( Map.Entry<?, ?> entry : ((Map<?, ?>)object).entrySet() ){ Object key = entry.getKey(); Object value = entry.getValue(); JsonArray entryContainer = new JsonArray(); entryContainer.add( convert( key, value == null ? null : value.getClass(), true ) ); entryContainer.add( convert( value, value == null ? null : value.getClass(), true ) ); array.add( entryContainer ); } if( serializeType ){ JsonObject container = new JsonObject(); container.add( "c", new JsonPrimitive( getTypeName( type ) ) ); container.add( "v", array ); return container; } else{ return array; } } private static Map<?, ?> deconvertObjectKeyMap( JsonElement element, Class<?> type ){ JsonArray jsonArray = element.getAsJsonArray(); if( SINGLETON_MAP_CLASS.equals( type ) ){ Object eKey = deconvert( jsonArray.get( 0 ).getAsJsonArray().get( 0 ), null ); Object eValue = deconvert( jsonArray.get( 0 ).getAsJsonArray().get( 1 ), null ); return Collections.singletonMap( eKey, eValue ); } else{ @SuppressWarnings("unchecked") Map<Object, Object> map = (Map<Object, Object>)instantiate( type ); for( int i = 0; i < jsonArray.size(); i++ ){ Object eKey = deconvert( jsonArray.get( i ).getAsJsonArray().get( 0 ), null ); Object eValue = deconvert( jsonArray.get( i ).getAsJsonArray().get( 1 ), null ); map.put( eKey, eValue ); } return map; } } private static JsonElement convertStringKeyMap( Object object, Class<?> type, boolean serializeType ){ JsonObject jsonObject = new JsonObject(); for( Map.Entry<?, ?> entry : ((Map<?, ?>)object).entrySet() ){ String key = (String)entry.getKey(); Object value = entry.getValue(); JsonElement valueElement = convert( value, value == null ? null : value.getClass(), true ); jsonObject.add( key, valueElement ); } if( serializeType ){ JsonObject container = new JsonObject(); container.add( "c", new JsonPrimitive( getTypeName( type ) ) ); container.add( "v", jsonObject ); return container; } else{ return jsonObject; } } private static Map<String, ?> deconvertStringKeyMap( JsonElement element, Class<?> type ){ JsonObject jsonObject = element.getAsJsonObject(); if( SINGLETON_MAP_CLASS.equals( type ) ){ Map.Entry<String, JsonElement> entry = jsonObject.entrySet().iterator().next(); String eKey = entry.getKey(); Object eValue = deconvert( entry.getValue(), null ); return Collections.singletonMap( eKey, eValue ); } else{ @SuppressWarnings("unchecked") Map<String, Object> map = (Map<String, Object>)instantiate( type ); for( Map.Entry<String, JsonElement> entry : jsonObject.entrySet() ){ String eKey = entry.getKey(); Object eValue = deconvert( entry.getValue(), null ); map.put( eKey, eValue ); } return map; } } private static JsonElement convertObject( Object object, Class<?> type, boolean serializeType ){ JsonObject jsonObject = new JsonObject(); for( Map.Entry<String, Field> entry : getFieldMap( object.getClass() ) .entrySet() ){ String name = entry.getKey(); Field field = entry.getValue(); field.setAccessible( true ); Object value = getValue( field, object ); boolean serializeFieldType = value != null && !field.getType().isPrimitive() && !value.getClass().equals( field.getType() ); JsonElement valueElement = convert( value, value == null ? null : value.getClass(), serializeFieldType ); jsonObject.add( name, valueElement ); } if( serializeType ){ JsonObject container = new JsonObject(); container.add( "c", new JsonPrimitive( getTypeName( type ) ) ); container.add( "v", jsonObject ); return container; } else{ return jsonObject; } } private static Object deconvertObject( JsonElement element, Class<?> type ){ JsonObject jsonObject = element.getAsJsonObject(); Object object = instantiate( type ); Map<String, Field> fieldMap = getFieldMap( object.getClass() ); for( Map.Entry<String, JsonElement> entry : jsonObject.entrySet() ){ String name = entry.getKey(); Field field = fieldMap.get( name ); field.setAccessible( true ); Object value = deconvert( entry.getValue(), field.getType() ); setValue( field, object, value ); } return object; } private static String getTypeName( Class<?> clazz ){ if( !clazz.isArray() ){ return clazz.getName(); } String result = "[]"; Class<?> componentType = clazz.getComponentType(); while( componentType.isArray() ){ result = result + "[]"; componentType = componentType.getComponentType(); } result = componentType.getName() + result; return result; } private static Class<?> getClass( String typeName ){ if( typeName.endsWith( "[]" ) ){ int index = typeName.indexOf( '[' ); String componentTypeName = typeName.substring( 0, index ); int levels = (typeName.length() - index) / 2; Class<?> componentType = loadClass( componentTypeName ); int[] dimensions = new int[levels]; return Array.newInstance( componentType, dimensions ).getClass(); } else{ return loadClass( typeName ); } } private static Class<?> loadClass( String className ){ if( boolean.class.getName().equals( className ) ){ return boolean.class; } else if( byte.class.getName().equals( className ) ){ return byte.class; } else if( short.class.getName().equals( className ) ){ return short.class; } else if( int.class.getName().equals( className ) ){ return int.class; } else if( long.class.getName().equals( className ) ){ return long.class; } else if( float.class.getName().equals( className ) ){ return float.class; } else if( double.class.getName().equals( className ) ){ return double.class; } else if( ARRAYS_ASLIST_CLASSNAME.equals( className ) ){ return ARRAYS_ASLIST_CLASS; } else if( SINGLETON_LIST_CLASSNAME.equals( className ) ){ return SINGLETON_LIST_CLASS; } else if( SINGLETON_SET_CLASSNAME.equals( className ) ){ return SINGLETON_SET_CLASS; } else if( SINGLETON_MAP_CLASSNAME.equals( className ) ){ return SINGLETON_MAP_CLASS; } else{ try{ return Thread.currentThread().getContextClassLoader().loadClass( className ); } catch( ClassNotFoundException e ){ throw new RuntimeException( e ); } } } private static boolean isContainer( JsonElement element ){ if( !element.isJsonObject() ){ return false; } JsonObject object = (JsonObject)element; return object.get( "c" ) != null && object.get( "v" ) != null; } private static boolean isSetOfStrings( Set<?> set ){ for( Object element : set ){ if( !(element == null || element instanceof String) ){ return false; } } return true; } private static Object instantiate( Class<?> clazz ){ try{ return clazz.newInstance(); } catch( InstantiationException e ){ throw new RuntimeException( e ); } catch( IllegalAccessException e ){ throw new RuntimeException( e ); } } private static Map<String, Field> getFieldMap( Class<?> clazz ){ Map<String, Field> fields = new LinkedTreeMap<String, Field>(); Class<?> current = clazz; String prefix = ""; while( !Object.class.equals( current ) ){ for( Field field : current.getDeclaredFields() ){ if( Modifier.isStatic( field.getModifiers() ) && Modifier.isFinal( field.getModifiers() ) ){ // ignore STATIC FINAL fields continue; } fields.put( prefix + field.getName(), field ); } current = current.getSuperclass(); prefix = prefix + "super."; } return fields; } private static void setValue( Field field, Object object, Object value ){ try{ field.set( object, value ); } catch( IllegalArgumentException e ){ throw new RuntimeException( e ); } catch( IllegalAccessException e ){ throw new RuntimeException( e ); } } private static Object getValue( Field field, Object object ){ try{ return field.get( object ); } catch( IllegalArgumentException e ){ throw new RuntimeException( e ); } catch( IllegalAccessException e ){ throw new RuntimeException( e ); } } private static Class<?> getInnermostComponentType( Class<?> type ){ if( !type.isArray() ){ return type; } else{ return type.getComponentType(); } } private static String formatDate( Date date ){ return createFormater().format( date ); } private static Date parseDate( String date ){ try{ return createFormater().parse( date ); } catch( ParseException e ){ throw new RuntimeException( "Unexpected date format " + date ); } } private static SimpleDateFormat createFormater(){ return new SimpleDateFormat( "dd.MM.yyyy HH:mm:ss.S" ); } }