package io.vivarium.serialization;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.vivarium.util.UUID;
public class SerializationEngine
{
public static final String ID_KEY = "uuid";
public static final String CLASS_KEY = "+class";
public static final Map<String, Object> EMPTY_OBJECT_MAP = Collections.unmodifiableMap(new HashMap<>());
public static final Map<VivariumObject, UUID> EMPTY_REFERENCE_MAP = Collections.unmodifiableMap(new HashMap<>());
public static final Map<UUID, VivariumObject> EMPTY_DEREFERENCE_MAP = Collections.unmodifiableMap(new HashMap<>());
private MapCollection _collection;
private HashMap<VivariumObject, UUID> _referenceMap;
private HashMap<UUID, VivariumObject> _dereferenceMap;
public SerializationEngine()
{
_collection = new MapCollection();
_referenceMap = new HashMap<>();
_dereferenceMap = new HashMap<>();
}
public void preDeserializeMap(HashMap<String, Object> map)
{
// Create an object
String clazzName = (String) map.get(CLASS_KEY);
VivariumObject object = makeUninitializedMapSerializer(clazzName);
// And store it into the idToReference map to allow later use (and circular references)
UUID uuid = UUID.fromString((String) map.get(ID_KEY));
storeIDToReference(uuid, object);
}
@SuppressWarnings("unchecked")
public VivariumObject deserializeMap(HashMap<String, Object> map)
{
// Copy the map and remove the class meta-parameter (leaving this on would cause later completeness checks to
// fail)
map = (HashMap<String, Object>) map.clone();
map.remove(CLASS_KEY);
// Locate the partially instantiated object created by the preDeserializeMap pass
UUID uuid = UUID.fromString((String) map.get(ID_KEY));
VivariumObject object = this.getReferenceObject(uuid);
// Deserialize the object
deserialize(object, map);
object.finalizeSerialization();
return object;
}
private VivariumObject makeUninitializedMapSerializer(String clazzName)
{
try
{
Class<? extends VivariumObject> clazz = ClassRegistry.getInstance().getClassNamed(clazzName);
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
return (VivariumObject) constructor.newInstance();
}
catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e)
{
e.printStackTrace();
throw new RuntimeException(
"Reflection during serialization failed. Unable to create new instance of " + clazzName + ". ", e);
}
}
/**
* Creates a SerializedCollection object from a MapSerializer.
*
* @param object
* @return
*/
public MapCollection serialize(VivariumObject object)
{
_collection = new MapCollection();
// Start by recursively serializing the top level object
serializeObjectIntoCollection(object);
return _collection;
}
/**
* Creates a SerializedCollection object from a MapSerializerCollection.
*
* @param object
* @return
*/
public MapCollection serialize(VivariumObjectCollection collection)
{
_collection = new MapCollection();
List<VivariumObject> list = collection.getAll(VivariumObject.class);
for (VivariumObject object : list)
{
serializeObjectIntoCollection(object);
}
return _collection;
}
private void serializeObjectIntoCollection(VivariumObject object)
{
try
{
if (!_referenceMap.containsKey(object))
{
storeReferenceToID(object);
HashMap<String, Object> map;
map = serializeMapSerializer(object);
_collection.addObject(map);
}
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
}
private void storeReferenceToID(VivariumObject object)
{
_referenceMap.put(object, object.getUUID());
}
private void storeIDToReference(UUID uuid, VivariumObject object)
{
_dereferenceMap.put(uuid, object);
}
private UUID getReferenceID(VivariumObject object)
{
return _referenceMap.get(object);
}
private VivariumObject getReferenceObject(UUID uuid)
{
return _dereferenceMap.get(uuid);
}
@SuppressWarnings("unchecked")
private Object serializeObject(Object object)
{
if (object == null)
{
return null;
}
Class<?> clazz = object.getClass();
// Serialize value
if (clazz.isArray())
{
// Do array crap here
LinkedList<Object> list = new LinkedList<>();
for (int i = 0; i < Array.getLength(object); i++)
{
Object arrayElement = Array.get(object, i);
Object serializedElement = serializeObject(arrayElement);
list.add(serializedElement);
}
return list;
}
else if (List.class.isAssignableFrom(clazz))
{
// Do list crap here
LinkedList<Object> list = new LinkedList<>();
for (Object element : (List<Object>) object)
{
list.add(serializeObject(element));
}
return list;
}
else if (VivariumObject.class.isAssignableFrom(clazz))
{
// Reference crap here
serializeObjectIntoCollection((VivariumObject) object);
return getReferenceID((VivariumObject) object).toString();
}
else if (Enum.class.isAssignableFrom(clazz))
{
// Enum crap
return "" + object;
}
else if (isPrimitive(clazz))
{
// Do nothing because this can be saved as is
return object;
}
else if (clazz == UUID.class)
{
return object.toString();
}
else
{
throw new UnsupportedOperationException("Cannot handle parameter type " + clazz);
}
}
private HashMap<String, Object> serializeMapSerializer(VivariumObject object) throws IllegalAccessException
{
HashMap<String, Object> map = new HashMap<>();
map.put(CLASS_KEY, "" + object.getClass().getSimpleName());
try
{
for (Field f : getSerializedParameters(object))
{
Object valueObject = f.get(object);
// Serialize value
Object serializedValue = serializeObject(valueObject);
// Add value to the map
if (valueObject != null)
{
map.put(f.getName().replaceAll("_", ""), serializedValue);
}
}
}
catch (
IllegalArgumentException e)
{
e.printStackTrace();
}
return map;
}
private boolean isPrimitive(Class<?> clazz)
{
return clazz.isPrimitive() || clazz == Boolean.class || clazz == Integer.class || clazz == Double.class;
}
private Set<Field> getSerializedParameters(VivariumObject object)
{
HashSet<Field> annotatedFields = new HashSet<>();
Class<?> clazz = object.getClass();
while (clazz != null)
{
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++)
{
Annotation[] annotations = fields[i].getAnnotations();
for (int j = 0; j < annotations.length; j++)
{
if (annotations[j].annotationType() == SerializedParameter.class)
{
fields[i].setAccessible(true);
annotatedFields.add(fields[i]);
}
}
}
clazz = clazz.getSuperclass();
}
return annotatedFields;
}
public static String getKeyFromFieldName(String fieldName)
{
return fieldName.substring(fieldName.lastIndexOf('_') + 1);
}
public void deserialize(VivariumObject object, Map<String, Object> map)
{
try
{
for (Field f : getSerializedParameters(object))
{
String attributeName = f.getName().replaceAll("_", "");
if (map.containsKey(attributeName))
{
Object valueObject = map.remove(attributeName);
Type fieldType = f.getGenericType();
valueObject = deserializeObject(valueObject, fieldType);
// Set value on the object
if (valueObject != null)
{
f.set(object, valueObject);
}
}
}
if (!map.isEmpty())
{
throw new IllegalArgumentException("Map has unused keys and values of " + map + " when constructing "
+ object.getClass().getSimpleName());
}
}
catch (IllegalAccessException | NoSuchMethodException | SecurityException | InstantiationException
| IllegalArgumentException | InvocationTargetException e)
{
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
private Object deserializeObject(Object object, Type type) throws NoSuchMethodException, SecurityException,
InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
{
Class<?> clazz;
if (type instanceof Class)
{
clazz = (Class<?>) type;
}
else if (type instanceof ParameterizedType)
{
clazz = (Class<?>) ((ParameterizedType) type).getRawType();
}
else
{
throw new IllegalStateException("Unable to derive Class information from " + type);
}
// object.equals(null) is used for object based representations of null which could be passed to the
// serialization engine.
if (object == null)
{
return null;
}
// Deserialize value
if (clazz.isArray())
{
// Do array crap here
int size = ((List<Object>) object).size();
Object array = Array.newInstance(clazz.getComponentType(), size);
int i = 0;
for (Object element : (List<Object>) object)
{
Object deserializedElement = deserializeObject(element, clazz.getComponentType());
try
{
Array.set(array, i, deserializedElement);
}
catch (Exception e)
{
e.printStackTrace();
System.out.println("size: " + size);
System.out.println("array: " + array);
System.out.println("deserializedElement: " + deserializedElement);
System.out.println("clazz: " + clazz);
System.out.println("clazz.getComponentType(): " + clazz.getComponentType());
System.out.println("element: " + element);
}
i++;
}
return array;
}
else if (List.class.isAssignableFrom(clazz))
{
// Do list crap here
Constructor<?> listConstructor = clazz.getConstructor();
List<Object> list = (List<Object>) listConstructor.newInstance();
Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
for (Object element : (List<Object>) object)
{
Object deserializedElement = deserializeObject(element, elementType);
list.add(deserializedElement);
}
return list;
}
else if (VivariumObject.class.isAssignableFrom(clazz))
{
// Reference crap here
return getReferenceObject(UUID.fromString((String) object));
}
else if (Enum.class.isAssignableFrom(clazz))
{
// Enum crap
@SuppressWarnings("rawtypes")
Enum valueEnum = Enum.valueOf((Class<Enum>) clazz, "" + object);
return valueEnum;
}
else if (isPrimitive(clazz))
{
// If this was saved or passed in as a primitive, but if it's in string form, we need to parse it
if (object.getClass() == String.class)
{
return parsePrimitive(clazz, (String) object);
}
else
{
return object;
}
}
else if (clazz == UUID.class)
{
return UUID.fromString((String) object);
}
else
{
throw new UnsupportedOperationException("Cannot handle parameter type " + clazz);
}
}
private Object parsePrimitive(Class<?> clazz, String s)
{
if (clazz == Boolean.class || clazz == boolean.class)
{
return Boolean.parseBoolean(s);
}
else if (clazz == Integer.class || clazz == int.class)
{
return Integer.parseInt(s);
}
else if (clazz == Double.class || clazz == double.class)
{
return Double.parseDouble(s);
}
else if (clazz == UUID.class)
{
return UUID.fromString(s);
}
else
{
throw new UnsupportedOperationException("Unable to parse primitive " + clazz);
}
}
/**
* Creates a deep copy of a vivarium object
*
* @param original
* The object to copy
* @return The copy of the original object
*/
@SuppressWarnings("unchecked")
public <T extends VivariumObject> T makeCopy(T original)
{
MapCollection collection = serialize(original);
VivariumObjectCollection collectionCopy = deserializeCollection(collection);
return (T) collectionCopy.getObject(original.getUUID());
}
/**
* Returns a List of MapSerializer objects from a serialized collection. The objects returned will be the objects
* with the highest relative serialization category ranking.
*
* @param collection
* @return
*/
public VivariumObjectCollection deserializeCollection(MapCollection collection)
{
_collection = collection;
VivariumObjectCollection objects = new VivariumObjectCollection();
for (HashMap<String, Object> map : collection)
{
preDeserializeMap(map);
}
for (HashMap<String, Object> map : collection)
{
VivariumObject object = deserializeMap(map);
objects.add(object);
}
return objects;
}
}