/**
*
*/
package xapi.model.impl;
import xapi.collect.X_Collect;
import xapi.collect.api.ClassTo;
import xapi.collect.api.IntTo;
import xapi.collect.api.ObjectTo;
import xapi.collect.api.StringTo;
import xapi.collect.proxy.CollectionProxy;
import xapi.collect.proxy.MapOf;
import xapi.dev.source.CharBuffer;
import xapi.fu.In2Out1;
import xapi.log.X_Log;
import xapi.model.api.Model;
import xapi.model.api.ModelDeserializationContext;
import xapi.model.api.ModelKey;
import xapi.model.api.ModelManifest;
import xapi.model.api.ModelSerializationContext;
import xapi.model.api.ModelSerializer;
import xapi.model.api.PrimitiveReader;
import xapi.model.api.PrimitiveSerializer;
import xapi.source.api.CharIterator;
import xapi.util.X_Debug;
import xapi.util.api.ConvertsTwoValues;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
public class ModelSerializerDefault <M extends Model> implements ModelSerializer<M>{
private final ClassTo<PrimitiveReader> primitiveReaders;
private final ClassTo<ConvertsTwoValues<Class, Class, Object>> collectionFactories;
public ModelSerializerDefault() {
this(X_Collect.newClassMap(PrimitiveReader.class));
}
public ModelSerializerDefault(final ClassTo<PrimitiveReader> primitiveReaders) {
this.primitiveReaders = primitiveReaders;
collectionFactories = X_Collect.newClassMap(Class.class.cast(ConvertsTwoValues.class));
}
@Override
public CharBuffer modelToString(final M model, final ModelSerializationContext ctx) {
final CharBuffer out = new CharBuffer();
ctx.getBuffer().addToEnd(out);
write(model, out, ctx);
return out;
}
protected void write(final M model, final CharBuffer out, final ModelSerializationContext ctx) {
final PrimitiveSerializer primitives = ctx.getPrimitives();
if (model == null) {
out.append(primitives.serializeInt(-2));
return;
}
final ModelKey modelKey = model.getKey();
if (modelKey == null) {
out.append(primitives.serializeInt(-1));
} else {
final String keyString = ctx.getService().keyToString(modelKey);
out.append(primitives.serializeString(keyString));
}
for (final String key : model.getPropertyNames()) {
if (preventSerialization(model, key, ctx)) {
continue;
}
final Object value = model.getProperty(key);
final Class<?> propertyType = model.getPropertyType(key);
writeObject(out, propertyType, value, primitives, ctx);
}
}
protected boolean preventSerialization(final M model, final String key, final ModelSerializationContext ctx) {
if (ctx.getManifest() == null) {
return false; // No manifest means we will just serialize all fields
}
final ModelManifest manifest = ctx.getManifest();
assert manifest.getMethodData(key) != null : "Invalid manifest; no data found for "+model.getType()+"."+key;
if (ctx.isClientToServer()) {
return !manifest.isClientToServerEnabled(key);
} else {
return !manifest.isServerToClientEnabled(key);
}
}
protected boolean preventDeserialization(final M model, final String key, final ModelDeserializationContext ctx) {
if (ctx.getManifest() == null) {
return false; // No manifest means we will just deserialize all fields
}
final ModelManifest manifest = ctx.getManifest();
assert manifest.getMethodData(key) != null : "Invalid manifest; no data found for "+model.getType()+"."+key;
if (ctx.isClientToServer()) {
return !manifest.isServerToClientEnabled(key);
} else {
return !manifest.isClientToServerEnabled(key);
}
}
protected boolean isSupportedEnumType(final Class<?> propertyType) {
return propertyType.isEnum();
}
/**
* This method is abstract so that providers in runtimes with limited / opt-in reflection
* support (like Gwt) will be able to implement a method that can perform a runtime lookup
* of a map to see if the given property type is indeed a model.
*/
protected boolean isModelType(final Class<?> propertyType) {
return Model.class.isAssignableFrom(propertyType);
}
protected boolean isIterableType(final Class<?> propertyType) {
return CollectionProxy.class.isAssignableFrom(propertyType);
}
protected void writeArray(final CharBuffer out, final Class<?> propertyType, final Object array, final PrimitiveSerializer primitives, final ModelSerializationContext ctx) {
if (array == null) {
out.append(primitives.serializeInt(-1));
return;
}
final int len = Array.getLength(array);
final String length = primitives.serializeInt(len);
out.append(length);
final Class<?> childType = propertyType.getComponentType();
if (childType.isPrimitive()) {
// For primitives, we will have to serialize those ourselves here, using array reflection
if (childType == boolean.class) {
// boolean[] is special. We want to write those with the serializer
out.append(primitives.serializeBooleanArray((boolean[])array));
} else {
// otherwise, we want to serialize a primitive. We will only bother with int, long, float and double,
// as all the small int types can just as easily be coerced to int; their size check will already have been done.
if (childType == double.class) {
for (int i = 0; i < len; i++) {
out.append(primitives.serializeDouble(Array.getDouble(array, i)));
}
} else if (childType == float.class) {
for (int i = 0; i < len; i++) {
out.append(primitives.serializeFloat(Array.getFloat(array, i)));
}
} else if (childType == long.class) {
for (int i = 0; i < len; i++) {
out.append(primitives.serializeLong(Array.getLong(array, i)));
}
} else {
// all int types
for (int i = 0; i < len; i++) {
out.append(primitives.serializeInt(Array.getInt(array, i)));
}
}
}
} else if (isModelType(childType)) {
for (int i = 0; i < len; i++) {
final Object model = Array.get(array, i);
writeModel(out, childType, (Model)model, primitives, ctx);
}
} else if (childType == String.class){
for (int i = 0; i < len; i++) {
writeString(out, (String)Array.get(array, i), primitives);
}
} else if (isSupportedEnumType(childType)){
// We are going to assume a homogenous array type...
assert childType != Enum.class : getClass()+" does not support Enum[] values from model "+propertyType;
for (int i = 0; i < len; i++) {
final Enum asEnum = (Enum) Array.get(array, i);
if (asEnum == null) {
// Null enum is going to be -1, an impossible ordinal
out.append(primitives.serializeInt(-1));
} else {
out.append(primitives.serializeInt(asEnum.ordinal()));
}
}
} else if (childType.isArray()) {
// An array of arrays. Oh boy...
for (int i = 0; i < len; i++) {
writeArray(out, propertyType.getComponentType(), Array.get(array, i), primitives, ctx);
}
} else {
throw new UnsupportedOperationException("Unable to serialize unsupported array type "+childType);
}
}
protected void writeIterable(final CharBuffer out, final CollectionProxy collection, final PrimitiveSerializer primitives, final ModelSerializationContext ctx) {
final Class keyType = collection.keyType();
final Class valueType = collection.valueType();
out.append(primitives.serializeClass(keyType));
out.append(primitives.serializeClass(valueType));
if (collection == null) {
out.append(primitives.serializeInt(-1));
return;
}
int len = collection.size();
final String length = primitives.serializeInt(len);
out.append(length);
if (len == 0) {
return;
}
if (keyType == Integer.class) {
// integer collection. If it is dense, we can just write the length and then the items.
if (collection.readWhileTrue(new In2Out1() {
int was;
@Override
public Boolean io(Object key, Object value) {
final Integer k = (Integer) key;
if (++was==k) {
return true;
}
return false;
}
})) {
// It is a dense array. We can write out the values
out.append(primitives.serializeBoolean(true));
collection.readWhileTrue((key, value)-> {
writeObject(out, valueType, value, primitives, ctx);
return true;
});
} else {
// it is a sparse array. write out w/ nulls
out.append(primitives.serializeBoolean(false));
collection.readWhileTrue((key, value) -> {
out.append(primitives.serializeInt((Integer) key));
writeObject(out, valueType, value, primitives, ctx);
return true;
}
);
}
} else if (keyType == Class.class) {
collection.readWhileTrue((key, value) -> {
out.append(primitives.serializeClass((Class) key));
writeObject(out, valueType, value, primitives, ctx);
return true;
});
} else if (keyType == String.class) {
collection.readWhileTrue((key, value) -> {
out.append(primitives.serializeString((String) key));
writeObject(out, valueType, value, primitives, ctx);
return true;
});
} else if (keyType.isEnum()) {
collection.readWhileTrue((key, value) -> {
out.append(primitives.serializeString(((Enum) key).name()));
writeObject(out, valueType, value, primitives, ctx);
return true;
});
} else {
assert false : "Unsupported key type "+keyType+" in model serializer: "+getClass();
}
}
protected Object readIterable(
Class propertyType,
CharIterator src,
PrimitiveSerializer primitives,
ModelDeserializationContext ctx
) {
final Class keyType = primitives.deserializeClass(src);
final Class valueType = primitives.deserializeClass(src);
int length = primitives.deserializeInt(src);
if (length == -1) {
// We are null
return null; // TODO: consider automatic never-nullness?
}
CollectionProxy result = newResult(propertyType, keyType, valueType);
if (length == 0) {
return result;
}
if (keyType == Integer.class) {
boolean dense = primitives.deserializeBoolean(src);
if (dense) {
// We can just push onto the array
for (int i = 0; i < length; i++) {
Object value = readObject(valueType, src, primitives, ctx);
result.setValue(i, value);
}
} else {
// we need to actually read the keys and set as appropriate
for (int i = 0; i < length; i++) {
int key = primitives.deserializeInt(src);
Object value = readObject(valueType, src, primitives, ctx);
result.setValue(key, value);
}
}
} else if (keyType == Class.class) {
for (int i = 0; i < length; i++) {
Class key = primitives.deserializeClass(src);
Object value = readObject(valueType, src, primitives, ctx);
result.setValue(key, value);
}
} else if (keyType == String.class) {
for (int i = 0; i < length; i++) {
String key = primitives.deserializeString(src);
Object value = readObject(valueType, src, primitives, ctx);
result.setValue(key, value);
}
} else if (keyType.isEnum()) {
for (int i = 0; i < length; i++) {
String key = primitives.deserializeString(src);
Object value = readObject(valueType, src, primitives, ctx);
final Enum enumKey = Enum.valueOf(valueType, key);
result.setValue(enumKey, value);
}
} else {
assert false : "Unsupported key type "+keyType+" in model serializer: "+getClass();
}
return result;
}
protected CollectionProxy newResult(Class collectionType, Class keyType, Class<?> valueType) {
if (collectionFactories.isEmpty()) {
initializeCollectionFactories(collectionFactories);
}
final ConvertsTwoValues<Class, Class, Object> factory = collectionFactories.get(collectionType);
return (CollectionProxy) factory.convert(keyType, valueType);
}
protected void initializeCollectionFactories(ClassTo<ConvertsTwoValues<Class,Class,Object>> factories) {
// TODO use whole-world compiler knowledge to erase factories we will never use, as this likely sucks in a lot of code.
// This would likely be best done in a ModelSerializerGwt that is generated and injected in place of this serializer
factories.put(IntTo.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
return X_Collect.newList(value);
}
});
factories.put(StringTo.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
if (value == StringTo.class) {
return X_Collect.newStringDeepMap(value);
}
return X_Collect.newStringMap(value);
}
});
factories.put(MapOf.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
return new MapOf(newMap(key, value), key, value);
}
});
factories.put(ObjectTo.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
return X_Collect.newMap(key, value);
}
});
factories.put(ClassTo.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
return X_Collect.newClassMap(value);
}
});
factories.put(IntTo.Many.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
return X_Collect.newIntMultiMap(value);
}
});
factories.put(StringTo.Many.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
return X_Collect.newStringMultiMap(value);
}
});
factories.put(ObjectTo.Many.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
return X_Collect.newMultiMap(key, value);
}
});
factories.put(ClassTo.Many.class, new ConvertsTwoValues<Class, Class, Object>() {
@Override
public Object convert(Class key, Class value) {
return X_Collect.newClassMultiMap(value);
}
});
}
protected Map newMap(Class key, Class value) {
return new LinkedHashMap();
}
protected void writeObject(
CharBuffer out,
Class valueType,
Object value,
PrimitiveSerializer primitives,
ModelSerializationContext ctx
) {
if (valueType.isArray()) {
// write an array
writeArray(out, valueType, value, primitives, ctx);
} else if (valueType == String.class) {
final String asString = (String) value;
writeString(out, asString, primitives);
} else if (valueType.isPrimitive()) {
// write a primitive
if (valueType == double.class) {
Double asDouble = (Double) value;
if (asDouble == null) {
asDouble = 0.;
}
out.append(primitives.serializeDouble(asDouble.doubleValue()));
} else if (valueType == float.class) {
Float asFloat = (Float) value;
if (asFloat == null) {
asFloat = 0f;
}
out.append(primitives.serializeFloat(asFloat.floatValue()));
} else if (valueType == long.class) {
Long asLong = (Long) value;
if (asLong == null) {
asLong = 0L;
}
out.append(primitives.serializeLong(asLong.longValue()));
} else {
Number asNumber = (Number) value;
if (asNumber == null) {
asNumber = 0;
}
// all int types
out.append(primitives.serializeInt(asNumber.intValue()));
}
} else if (isModelType(valueType)) {
writeModel(out, valueType, (Model)value, primitives, ctx);
} else if (isIterableType(valueType)) {
writeIterable(out, (CollectionProxy)value, primitives, ctx);
} else if (isSupportedEnumType(valueType)) {
if (value == null) {
out.append(primitives.serializeInt(-1));
} else {
final Enum asEnum = (Enum) value;
out.append(primitives.serializeInt(asEnum.ordinal()));
}
} else {
assert false : "Unserializable field type: "+valueType;
}
}
protected void writeString(final CharBuffer out, final String string, final PrimitiveSerializer primitives) {
if (string == null) {
out.append(primitives.serializeInt(-1));
} else {
out.append(primitives.serializeInt(string.length()));
out.append(string);
}
}
@SuppressWarnings({
"rawtypes", "unchecked"
})
protected void writeModel(final CharBuffer out, final Class<?> propertyType, final Model childModel, final PrimitiveSerializer primitives, final ModelSerializationContext ctx) {
final ModelSerializer serializer = newSerializer(Class.class.cast(propertyType), ctx);
final CharBuffer was = ctx.getBuffer();
ctx.setBuffer(out);
try {
serializer.modelToString(childModel, ctx);
} finally {
ctx.setBuffer(was);
}
}
protected <Mod extends Model> ModelSerializer<Mod> newSerializer(final Class<Mod> propertyType, final ModelSerializationContext ctx) {
return new ModelSerializerDefault<Mod>(primitiveReaders);
}
@Override
@SuppressWarnings("unchecked")
public M modelFromString(final CharIterator src, final ModelDeserializationContext ctx) {
final PrimitiveSerializer primitives = ctx.getPrimitives();
final int modelState = primitives.deserializeInt(src);
if (modelState == -2) {
return null;
}
ModelKey key;
if (modelState > -1) {
// There is a key for this model
final String keyString = src.consume(modelState).toString();
key = ctx.getService().keyFromString(keyString);
} else {
key = null;
}
final M model = (M) ctx.getModel();
assert model != null : "Null model found "+src;
model.setKey(key);
for (final String propertyName : model.getPropertyNames()) {
if (preventDeserialization(model, propertyName, ctx)) {
continue;
}
readProperty(model, propertyName, src, ctx);
}
return model;
}
protected void readProperty(final Model model, final String propertyName, final CharIterator src, final ModelDeserializationContext ctx) {
final Class<?> propertyType = model.getPropertyType(propertyName);
final PrimitiveSerializer primitives = ctx.getPrimitives();
if (propertyType.isArray()) {
model.setProperty(propertyName, readArray(propertyType.getComponentType(), src, primitives, ctx));
} else if (propertyType.isPrimitive()) {
final Object value = readPrimitive(propertyType, src, primitives);
model.setProperty(propertyName, value);
} else {
final Object value = readObject(propertyType, src, primitives, ctx);
model.setProperty(propertyName, value);
}
}
/**
* @param componentType
* @param src
* @param primitives
* @param ctx
* @return
*/
protected Object readArray(final Class<?> componentType, final CharIterator src, final PrimitiveSerializer primitives,
final ModelDeserializationContext ctx) {
final int len = primitives.deserializeInt(src);
if (len == -1) {
return null;
}
if (componentType.isPrimitive()) {
final Object array = Array.newInstance(componentType, len);
for (int i = 0;i < len;i++) {
if (componentType == int.class) {
Array.setInt(array, i, primitives.deserializeInt(src));
} else if (componentType == float.class) {
Array.setFloat(array, i, primitives.deserializeFloat(src));
} else if (componentType == boolean.class) {
Array.setBoolean(array, i, primitives.deserializeBoolean(src));
} else if (componentType == char.class) {
Array.setChar(array, i, primitives.deserializeChar(src));
} else if (componentType == double.class) {
Array.setDouble(array, i, primitives.deserializeDouble(src));
} else if (componentType == long.class) {
Array.setLong(array, i, primitives.deserializeLong(src));
} else if (componentType == short.class) {
Array.setShort(array, i, primitives.deserializeShort(src));
} else if (componentType == byte.class) {
Array.setByte(array, i, primitives.deserializeByte(src));
} else {
throw new UnsupportedOperationException("Unsupported array component type"+componentType);
}
}
return array;
} else {
final Object array = Array.newInstance(componentType, len);
for (int i = 0;i < len;i++) {
final Object value = readObject(componentType, src, primitives, ctx);
Array.set(array, i, value);
}
return array;
}
}
/**
* @param componentType
* @param src
* @param primitives
* @return
*/
protected Object readPrimitive(final Class<?> componentType, final CharIterator src, final PrimitiveSerializer primitives) {
final PrimitiveReader reader = getPrimitiveReader(componentType, primitiveReaders);
return reader.readPrimitive(componentType, src, primitives);
}
protected PrimitiveReader getPrimitiveReader(final Class<?> componentType, final ClassTo<PrimitiveReader> primitiveReaders) {
PrimitiveReader reader = primitiveReaders.get(componentType);
if (reader == null) {
if (componentType == int.class) {
reader = PrimitiveReaders.forInt();
} else if (componentType == float.class) {
reader = PrimitiveReaders.forFloat();
} else if (componentType == boolean.class) {
reader = PrimitiveReaders.forBoolean();
} else if (componentType == char.class) {
reader = PrimitiveReaders.forChar();
} else if (componentType == double.class) {
reader = PrimitiveReaders.forDouble();
} else if (componentType == long.class) {
reader = PrimitiveReaders.forLong();
} else if (componentType == short.class) {
reader = PrimitiveReaders.forShort();
} else if (componentType == byte.class) {
reader = PrimitiveReaders.forByte();
} else {
throw new UnsupportedOperationException("Unsupported primitive type "+componentType);
}
primitiveReaders.put(componentType, reader);
}
return reader;
}
/**
* @param propertyType
* @param src
* @param ctx
* @param primitives
* @return
*/
@SuppressWarnings({
"unchecked", "rawtypes"
})
protected Object readObject(final Class propertyType, final CharIterator src, final PrimitiveSerializer primitives, final ModelDeserializationContext ctx) {
if (propertyType == String.class) {
final int len = primitives.deserializeInt(src);
if (len == -1) {
return null;
}
return src.consume(len);
} else if (isModelType(propertyType)) {
// We have an inner model to read!
final ModelDeserializationContext context = ctx.createChildContext(propertyType, src);
return modelFromString(src, context);
} else if (propertyType.isArray()) {
return readArray(propertyType.getComponentType(), src, primitives, ctx);
} else if (isIterableType(propertyType)) {
return readIterable(propertyType, src, primitives, ctx);
} else if (propertyType.isEnum()) {
// No great way to deserialize enums without reflection, so lets leave a hook for environments
// where reflection is not possible or preferable can implement a mapping of enum class to enum values[]...
return readEnum(propertyType, src, primitives, ctx);
}
throw new UnsupportedOperationException("Unable to deserialize object of type "+propertyType);
}
/**
* @param propertyType
* @param src
* @param primitives
* @param ctx
* @return
*/
@SuppressWarnings("unchecked")
protected Object readEnum(final Class propertyType, final CharIterator src, final PrimitiveSerializer primitives,
final ModelDeserializationContext ctx) {
try {
final int ordinal = primitives.deserializeInt(src);
if (ordinal == -1) {
return null;
}
final Method method = propertyType.getMethod("values");
final Object values = method.invoke(null);
return Array.get(values, ordinal);
} catch (final Throwable e) {
X_Log.error(getClass(), "Error reading enum "+propertyType+" from "+src);
throw X_Debug.rethrow(e);
}
}
}