/*
* Copyright 2014 TWO SIGMA OPEN SOURCE, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.twosigma.beaker.jvm.serialization;
import com.twosigma.beaker.BeakerCodeCell;
import com.twosigma.beaker.jvm.object.OutputCell;
import com.twosigma.beaker.chart.Color;
import com.twosigma.beaker.easyform.EasyForm;
import com.twosigma.beaker.jvm.object.BeakerDashboard;
import com.twosigma.beaker.jvm.object.CyclingOutputContainerLayoutManager;
import com.twosigma.beaker.jvm.object.EvaluationResult;
import com.twosigma.beaker.jvm.object.GridOutputContainerLayoutManager;
import com.twosigma.beaker.jvm.object.OutputContainer;
import com.twosigma.beaker.jvm.object.OutputContainerCell;
import com.twosigma.beaker.jvm.object.TabbedOutputContainerLayoutManager;
import com.twosigma.beaker.table.TableDisplay;
import com.twosigma.beaker.jvm.object.UpdatableEvaluationResult;
import com.twosigma.beaker.jvm.object.DashboardLayoutManager;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class BasicObjectSerializer implements BeakerObjectConverter {
public static final String TYPE_INTEGER = "integer";
public static final String TYPE_LONG = "int64";
public static final String TYPE_BIGINT = "bigint";
public static final String TYPE_DOUBLE = "double";
public static final String TYPE_STRING = "string";
public static final String TYPE_BOOLEAN = "boolean";
public static final String TYPE_TIME = "time";
public static final String TYPE_SELECT = "select";
private final static Logger logger = LoggerFactory.getLogger(BasicObjectSerializer.class.getName());
protected final Map<String, String> types;
protected final List<String> knownBeakerTypes;
protected final List<ObjectDeserializer> supportedDeserializers;
protected final List<ObjectSerializer> supportedSerializers;
protected final ThreadLocal<Map<String, String>> threadTypes;
protected final ThreadLocal<List<ObjectDeserializer>> threadDeserializers;
protected final ThreadLocal<List<ObjectSerializer>> threadSerializers;
protected boolean isListOfPrimitiveTypeMaps(Object o) {
if (!(o instanceof Collection<?>))
return false;
Collection<?> c = (Collection<?>) o;
if (c.isEmpty())
return false;
for (Object obj : c) {
if (obj != null && !isPrimitiveTypeMap(obj)) {
return false;
}
}
return true;
}
protected boolean isPrimitiveTypeMap(Object o) {
if (!(o instanceof Map<?, ?>))
return false;
Map<?, ?> m = (Map<?, ?>) o;
Set<?> eset = m.entrySet();
for (Object entry : eset) {
Entry<?, ?> e = (Entry<?, ?>) entry;
if (e.getValue() != null && !isPrimitiveType(e.getValue().getClass().getName()))
return false;
}
return true;
}
protected boolean isPrimitiveTypeListOfList(Object o) {
if (!(o instanceof Collection<?>))
return false;
Collection<?> m = (Collection<?>) o;
int max = 0;
for (Object entry : m) {
if (!(entry instanceof Collection<?>))
return false;
Collection<?> e = (Collection<?>) entry;
for (Object ei : e) {
if (ei != null && !isPrimitiveType(ei.getClass().getName()))
return false;
}
if (max < e.size())
max = e.size();
}
return max >= 2 && m.size() >= 2;
}
public BasicObjectSerializer() {
types = new HashMap<String, String>();
threadDeserializers = new ThreadLocal<List<ObjectDeserializer>>();
threadSerializers = new ThreadLocal<List<ObjectSerializer>>();
supportedDeserializers = new ArrayList<ObjectDeserializer>();
supportedSerializers = new ArrayList<ObjectSerializer>();
threadTypes = new ThreadLocal<Map<String, String>>();
knownBeakerTypes = new ArrayList<String>();
addTypeConversion("java.lang.Boolean", TYPE_BOOLEAN);
addTypeConversion("java.lang.Byte", TYPE_INTEGER);
addTypeConversion("java.lang.Character", TYPE_STRING);
addTypeConversion("java.lang.Double", TYPE_DOUBLE);
addTypeConversion("java.lang.Enum", TYPE_SELECT);
addTypeConversion("java.lang.Float", TYPE_DOUBLE);
addTypeConversion("java.lang.Integer", TYPE_INTEGER);
addTypeConversion("java.lang.Long", TYPE_LONG);
addTypeConversion("java.lang.Short", TYPE_INTEGER);
addTypeConversion("java.lang.String", TYPE_STRING);
addTypeConversion("java.lang.StringBuffer", TYPE_STRING);
addTypeConversion("java.lang.StringBuilder", TYPE_STRING);
addTypeConversion("java.util.Date", TYPE_TIME);
addTypeConversion("java.util.concurrent.atomic.AtomicInteger", TYPE_INTEGER);
addTypeConversion("java.util.concurrent.atomic.AtomicLong", TYPE_INTEGER);
addTypeConversion("java.math.BigDecimal", TYPE_DOUBLE);
addTypeConversion("java.math.BigInteger", TYPE_BIGINT);
addTypeConversion("org.codehaus.groovy.runtime.GStringImpl", TYPE_STRING);
addTypeSerializer(new PrimitiveTypeSerializer());
addTypeSerializer(new ListOfPrimitiveTypeMapsSerializer(this));
addTypeSerializer(new PrimitiveTypeListOfListSerializer());
addTypeSerializer(new PrimitiveTypeMapSerializer());
addTypeSerializer(new ArraySerializer(this));
addTypeSerializer(new CollectionSerializer(this));
addTypeSerializer(new MapSerializer(this));
}
@Override
public String convertType(String tn) {
if (threadTypes.get()!=null && threadTypes.get().containsKey(tn))
return threadTypes.get().get(tn);
if (types.containsKey(tn))
return types.get(tn);
return "";
}
@Override
public boolean isPrimitiveType(String tn) {
return types.containsKey(tn) || (threadTypes.get()!=null && threadTypes.get().containsKey(tn));
}
@Override
public boolean writeObject(Object obj, JsonGenerator jgen, boolean expand)
throws IOException {
if (obj == null) {
jgen.writeNull();
} else if ((obj instanceof TableDisplay) ||
(obj instanceof EvaluationResult) ||
(obj instanceof UpdatableEvaluationResult) ||
(obj instanceof BeakerCodeCell) ||
(obj instanceof ImageIcon) ||
(obj instanceof Date) ||
(obj instanceof BeakerDashboard) ||
(obj instanceof BufferedImage) ||
(obj instanceof TabbedOutputContainerLayoutManager) ||
(obj instanceof GridOutputContainerLayoutManager) ||
(obj instanceof CyclingOutputContainerLayoutManager) ||
(obj instanceof DashboardLayoutManager) ||
(obj instanceof OutputContainerCell) ||
(obj instanceof OutputContainer) ||
(obj instanceof EasyForm) ||
(obj instanceof Color)) {
logger.debug("basic object");
jgen.writeObject(obj);
} else
return runThreadSerializers(obj, jgen, expand) || runConfiguredSerializers(obj,
jgen,
expand);
return true;
}
public boolean runThreadSerializers(Object obj, JsonGenerator jgen, boolean expand) throws IOException, JsonProcessingException {
if (threadSerializers.get() == null)
return false;
for (ObjectSerializer s : threadSerializers.get()) {
try {
if (s.canBeUsed(obj, expand) && s.writeObject(obj, jgen, expand)) {
logger.debug("used thread serialization");
return true;
}
} catch (Exception e) {
logger.error("exception in thread serialization", e);
}
}
return false;
}
public boolean runConfiguredSerializers(Object obj, JsonGenerator jgen, boolean expand) throws IOException, JsonProcessingException {
for (ObjectSerializer s : supportedSerializers) {
if (s.canBeUsed(obj, expand) && s.writeObject(obj, jgen, expand))
return true;
}
return false;
}
@Override
public Object deserialize(JsonNode n, ObjectMapper mapper) {
if (n==null)
return null;
Object obj = null;
if(threadDeserializers.get()!=null) {
for (ObjectDeserializer d : threadDeserializers.get()) {
try {
if (d.canBeUsed(n)) {
obj = d.deserialize(n, mapper);
if (obj != null) {
logger.debug("used thread deserialization");
break;
}
}
} catch (Exception e) {
logger.error("exception in thread deserialization",e);
obj = null;
}
}
}
if (obj!=null)
return obj;
for (ObjectDeserializer d : supportedDeserializers) {
try {
if (d.canBeUsed(n)) {
obj = d.deserialize(n, mapper);
if (obj != null) {
logger.debug("used custom deserialization");
break;
}
}
} catch (Exception e) {
logger.error("exception in deserialization",e);
obj = null;
}
}
if (obj==null) {
logger.debug("using standard deserialization");
try {
obj = mapper.readValue(n.asText(), Object.class);
} catch (Exception e) {
logger.error("exception in auto deserialization",e);
obj = null;
}
}
return obj;
}
/*
* (non-Javadoc)
* These implement module behavior modification
*/
@Override
public void addTypeConversion(String from, String to) {
types.put(from,to);
}
@Override
public void addTypeDeserializer(ObjectDeserializer o) {
supportedDeserializers.add(o);
}
@Override
public void addTypeSerializer(ObjectSerializer o) {
supportedSerializers.add(o);
}
@Override
public void addfTypeDeserializer(ObjectDeserializer o) {
supportedDeserializers.add(0,o);
}
@Override
public void addfTypeSerializer(ObjectSerializer o) {
supportedSerializers.add(0,o);
}
/*
* (non-Javadoc)
* These implement thread specific module behavior modification
*/
@Override
public void addThreadSpecificTypeConversion(String from, String to) {
if (threadTypes.get()==null)
threadTypes.set(new HashMap<String,String>());
threadTypes.get().put(from, to);
}
@Override
public void addThreadSpecificTypeDeserializer(ObjectDeserializer o) {
if (threadDeserializers.get()==null)
threadDeserializers.set(new ArrayList<ObjectDeserializer>());
threadDeserializers.get().add(o);
}
@Override
public void addThreadSpecificTypeSerializer(ObjectSerializer o) {
if (threadSerializers.get()==null)
threadSerializers.set(new ArrayList<ObjectSerializer>());
threadSerializers.get().add(o);
}
/*
* (non-Javadoc)
* These are the default auto-transforming serializers
*/
class PrimitiveTypeSerializer implements ObjectSerializer {
@Override
public boolean canBeUsed(Object obj, boolean expand) {
return isPrimitiveType(obj.getClass().getName());
}
@Override
public boolean writeObject(Object obj, JsonGenerator jgen, boolean expand) throws JsonProcessingException, IOException {
jgen.writeObject(obj);
return true;
}
}
class ListOfPrimitiveTypeMapsSerializer implements ObjectSerializer {
private final BasicObjectSerializer parent;
public ListOfPrimitiveTypeMapsSerializer(BasicObjectSerializer p) {
parent = p;
}
@Override
public boolean canBeUsed(Object obj, boolean expand) {
return expand && isListOfPrimitiveTypeMaps(obj);
}
@Override
public boolean writeObject(Object obj, JsonGenerator jgen, boolean expand) throws JsonProcessingException, IOException {
logger.debug("list of maps");
try {
// convert this 'on the fly' to a datatable
@SuppressWarnings("unchecked")
Collection<Map<?, ?>> co = (Collection<Map<?, ?>>) obj;
TableDisplay t = new TableDisplay(co,parent);
jgen.writeObject(t);
return true;
} catch(Exception e) {
return false;
}
}
}
class PrimitiveTypeListOfListSerializer implements ObjectSerializer {
@Override
public boolean canBeUsed(Object obj, boolean expand) {
return expand && isPrimitiveTypeListOfList(obj);
}
@Override
public boolean writeObject(Object obj, JsonGenerator jgen, boolean expand) throws JsonProcessingException, IOException {
logger.debug("collection of collections");
Collection<?> m = (Collection<?>) obj;
int max = 0;
for (Object entry : m) {
Collection<?> e = (Collection<?>) entry;
if (max < e.size())
max = e.size();
}
List<String> columns = new ArrayList<String>();
for (int i=0; i<max; i++)
columns.add("c"+i);
List<List<?>> values = new ArrayList<List<?>>();
for (Object entry : m) {
Collection<?> e = (Collection<?>) entry;
List<Object> l2 = new ArrayList<Object>(e);
if (l2.size() < max) {
for (int i=l2.size(); i<max; i++)
l2.add(null);
}
values.add(l2);
}
jgen.writeStartObject();
jgen.writeObjectField("type", "TableDisplay");
jgen.writeObjectField("columnNames", columns);
jgen.writeObjectField("values", values);
jgen.writeObjectField("subtype", TableDisplay.MATRIX_SUBTYPE);
jgen.writeEndObject();
return true;
}
}
class PrimitiveTypeMapSerializer implements ObjectSerializer {
@Override
public boolean canBeUsed(Object obj, boolean expand) {
return expand && isPrimitiveTypeMap(obj);
}
@Override
public boolean writeObject(Object obj, JsonGenerator jgen, boolean expand) throws JsonProcessingException, IOException {
logger.debug("primitive type map");
Map<?,?> m = (Map<?,?>) obj;
List<String> columns = new ArrayList<String>();
columns.add("Key");
columns.add("Value");
List<List<?>> values = new ArrayList<List<?>>();
Set<?> eset = m.entrySet();
for (Object entry : eset) {
Entry<?,?> e = (Entry<?, ?>) entry;
List<Object> l = new ArrayList<Object>();
Object o = e.getKey();
l.add(null==o?"null":o.toString());
l.add(e.getValue());
values.add(l);
}
jgen.writeStartObject();
jgen.writeObjectField("type", "TableDisplay");
jgen.writeObjectField("columnNames", columns);
jgen.writeObjectField("values", values);
jgen.writeObjectField("subtype", TableDisplay.DICTIONARY_SUBTYPE);
jgen.writeEndObject();
return true;
}
}
class ArraySerializer implements ObjectSerializer {
private final BasicObjectSerializer parent;
public ArraySerializer(BasicObjectSerializer p) {
parent = p;
}
@Override
public boolean canBeUsed(Object obj, boolean expand) {
return obj.getClass().isArray();
}
@Override
public boolean writeObject(Object obj, JsonGenerator jgen, boolean expand) throws JsonProcessingException, IOException {
logger.debug("array");
// write out an array of objects.
jgen.writeStartArray();
final int length = Array.getLength(obj);
for (int i = 0; i < length; ++i) {
Object o = Array.get(obj, i);
if (!parent.writeObject(o, jgen, false)) {
jgen.writeObject(o.toString());
}
}
jgen.writeEndArray();
return true;
}
}
class CollectionSerializer implements ObjectSerializer {
private final BasicObjectSerializer parent;
public CollectionSerializer(BasicObjectSerializer p) {
parent = p;
}
@Override
public boolean canBeUsed(Object obj, boolean expand) {
return obj instanceof Collection<?>;
}
@Override
public boolean writeObject(Object obj, JsonGenerator jgen, boolean expand) throws JsonProcessingException, IOException {
logger.debug("collection");
// convert this 'on the fly' to an array of objects
Collection<?> c = (Collection<?>) obj;
jgen.writeStartArray();
for(Object o : c) {
if (!parent.writeObject(o, jgen, false))
jgen.writeObject(o.toString());
}
jgen.writeEndArray();
return true;
}
}
class MapSerializer implements ObjectSerializer {
private final BasicObjectSerializer parent;
public MapSerializer(BasicObjectSerializer p) {
parent = p;
}
@Override
public boolean canBeUsed(Object obj, boolean expand) {
return obj instanceof Map<?,?>;
}
@Override
public boolean writeObject(Object obj, JsonGenerator jgen, boolean expand) throws JsonProcessingException, IOException {
logger.debug("generic map");
// convert this 'on the fly' to a map of objects
Map<?,?> m = (Map<?,?>) obj;
Set<?> kset = m.keySet();
if (kset.size()==0 || !(kset.iterator().next() instanceof String))
jgen.writeObject(obj.toString());
else {
jgen.writeStartObject();
for (Object k : kset) {
jgen.writeFieldName((null==k)?"null":k.toString());
if (!parent.writeObject(m.get(k), jgen, false))
jgen.writeObject(m.get(k)!=null ? (m.get(k).toString()) : "null");
}
jgen.writeEndObject();
}
return true;
}
}
@Override
public void addKnownBeakerType(String t) {
knownBeakerTypes.add(t);
}
@Override
public boolean isKnownBeakerType(String t) {
return knownBeakerTypes.contains(t);
}
}