/*
* Copyright (c) 2012 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* HUMBOLDT EU Integrated Project #030962
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.hale.common.instance.orient.internal;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import javax.xml.namespace.QName;
import org.springframework.core.convert.ConversionService;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.ORecordAbstract;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ORecordBytes;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKBWriter;
import de.fhg.igd.osgi.util.OsgiUtils;
import eu.esdihumboldt.hale.common.core.HalePlatform;
import eu.esdihumboldt.hale.common.instance.geometry.DefaultGeometryProperty;
import eu.esdihumboldt.hale.common.instance.model.Group;
import eu.esdihumboldt.hale.common.instance.model.Instance;
import eu.esdihumboldt.hale.common.instance.orient.OGroup;
import eu.esdihumboldt.hale.common.instance.orient.OInstance;
import eu.esdihumboldt.hale.common.schema.geometry.CRSDefinition;
import eu.esdihumboldt.hale.common.schema.geometry.GeometryProperty;
import eu.esdihumboldt.hale.common.schema.model.ChildDefinition;
import eu.esdihumboldt.util.Identifiers;
/**
* Serialization helper for storing values not support by OrientDB. Serializes
* geometries as WKB and holds a runtime cache for CRSs.
*
* @author Simon Templer
*/
public abstract class OSerializationHelper {
/**
* Store information on how to convert a value back to its original form.
*/
public static class ConvertProxy {
private final ConversionService cs;
private final Class<? extends Object> original;
/**
* Create a convert proxy
*
* @param cs the conversion service to use
* @param original the original type
*/
public ConvertProxy(ConversionService cs, Class<? extends Object> original) {
this.cs = cs;
this.original = original;
}
/**
* Convert the string to its original form.
*
* @param value the string value
* @return the converted value
*/
public Object convert(String value) {
return cs.convert(value, original);
}
/**
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((original == null) ? 0 : original.hashCode());
return result;
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ConvertProxy other = (ConvertProxy) obj;
if (original == null) {
if (other.original != null)
return false;
}
else if (!original.equals(other.original))
return false;
return true;
}
}
/**
* Collection types
*/
private static enum CollectionType {
SET, LIST
}
/**
* Cache for resolved classes for deserialization
*/
private static final LinkedHashMap<String, Class<?>> resolved = new LinkedHashMap<String, Class<?>>();
/**
* Binary wrapper class name
*/
public static final String BINARY_WRAPPER_CLASSNAME = "___BinaryWrapper___";
/**
* Binary wrapper class field name
*/
public static final String BINARY_WRAPPER_FIELD = "___bin___";
/**
* Field specifying the serialization type
*/
public static final String FIELD_SERIALIZATION_TYPE = "___st___";
/**
* Java object serialization (Swiss army knife)
*/
private static final int SERIALIZATION_TYPE_JAVA = 0;
/**
* Converted value
*/
private static final int SERIALIZATION_TYPE_STRING = 1;
/**
* WKB geometry
*/
private static final int SERIALIZATION_TYPE_GEOM = 2;
/**
* Geometry property (WKB + optional CRS ID)
*/
private static final int SERIALIZATION_TYPE_GEOM_PROP = 3;
/**
* Collection property
*/
private static final int SERIALIZATION_TYPE_COLLECTION = 4;
/**
* Byte array property
*/
private static final int SERIALIZATION_TYPE_BYTEARRAY = 5;
/**
* Field specifying the CRS ID
*/
public static final String FIELD_CRS_ID = "___crs___";
/**
* Field specifying the converter ID
*/
public static final String FIELD_CONVERT_ID = "___cnv___";
/**
* Field specifying the string value
*/
public static final String FIELD_STRING_VALUE = "___str___";
/**
* Field holding the collection type
*/
public static final String FIELD_COLLECTION_TYPE = "___clc___";
/**
* Field holding the values of a collection
*/
public static final String FIELD_VALUES = "___vls___";
/**
* Runtime identifiers for CRSs
*/
private static final Identifiers<CRSDefinition> CRS_IDS = new Identifiers<CRSDefinition>("crs",
true);
/**
* Runtime identifiers for {@link ConvertProxy}s
*/
private static final Identifiers<ConvertProxy> CONVERTER_IDS = new Identifiers<ConvertProxy>(
"cnv", true);
/**
* String conversion white list
*/
private static final Set<Class<?>> CONV_WHITE_LIST = new HashSet<Class<?>>();
static {
CONV_WHITE_LIST.add(BigInteger.class);
CONV_WHITE_LIST.add(URI.class);
}
/**
* Determines if the given field type is supported directly by the database
*
* @param type the field type
* @return if the field type is supported
*/
private static boolean isSupportedFieldType(Class<? extends Object> type) {
// records
if (ORecordAbstract.class.isAssignableFrom(type)) {
return true;
}
// primitives and arrays
else if (type.isPrimitive() || type.isArray()) {
return true;
}
// wrapper types
else if (Double.class.isAssignableFrom(type) || Float.class.isAssignableFrom(type)
|| Integer.class.isAssignableFrom(type) || Long.class.isAssignableFrom(type)
|| Short.class.isAssignableFrom(type) || Byte.class.isAssignableFrom(type)
|| String.class.isAssignableFrom(type) || Boolean.class.isAssignableFrom(type)) {
return true;
}
// date
/*
* XXX OrientDB strips time information from dates. To avoid information
* loss, we serialize dates and derivatives manually instead
*/
// else if (Date.class.isAssignableFrom(type)) {
// return true;
// }
// collections
else if (Collection.class.isAssignableFrom(type)) {
/*
* XXX OrientDB can't deal with nested collections/lists!(?) as a
* work-around we also serialize collections
*/
// return true;
}
return false;
}
/**
* Prepare a value not supported as field in OrientDB so it can be stored in
* the database.
*
* @param value the value to convert
* @return the converted value that may be used as a property value
*/
public static Object convertForDB(Object value) {
if (value == null)
return null;
if (value instanceof OGroup) {
// special case: if possible use the internal document for
// OGroup/OInstance
return ((OGroup) value).getDocument();
}
else if (value instanceof Instance) {
OInstance tmp = new OInstance((Instance) value);
return tmp.getDocument();
}
else if (value instanceof Group) {
OGroup tmp = new OGroup((Group) value);
return tmp.getDocument();
}
else if (isSupportedFieldType(value.getClass())) {
return value;
}
return serialize(value);
}
/**
* Serialize and/or wrap a value not supported as field in OrientDB so it
* can be stored in the database.
*
* @param value the value to serialize
* @return the document wrapping the value
*/
public static ODocument serialize(Object value) {
/*
* As collections of ORecordBytes are not supported (or rather of
* records that are no documents, see embeddedCollectionToStream in
* ORecordSerializerCSVAbstract ~578) they are wrapped in a document.
*/
ODocument doc = new ODocument();
// try conversion to string first
final ConversionService cs = HalePlatform.getService(ConversionService.class);
if (cs != null) {
// check if conversion allowed and possible
if (CONV_WHITE_LIST.contains(value.getClass())
&& cs.canConvert(value.getClass(), String.class)
&& cs.canConvert(String.class, value.getClass())) {
String stringValue = cs.convert(value, String.class);
ConvertProxy convert = new ConvertProxy(cs, value.getClass());
doc.field(FIELD_CONVERT_ID, CONVERTER_IDS.getId(convert));
doc.field(FIELD_SERIALIZATION_TYPE, SERIALIZATION_TYPE_STRING);
doc.field(FIELD_STRING_VALUE, stringValue);
return doc;
}
}
if (value instanceof Collection) {
CollectionType type = null;
if (value instanceof List) {
type = CollectionType.LIST;
}
else if (value instanceof Set) {
type = CollectionType.SET;
}
if (type != null) {
// wrap collection values
Collection<?> elements = (Collection<?>) value;
List<Object> values = new ArrayList<Object>();
for (Object element : elements) {
Object convElement = convertForDB(element);
values.add(convElement);
}
// set values
// XXX ok to always use EMBEDDEDLIST as type?
doc.field(FIELD_VALUES, values, OType.EMBEDDEDLIST);
doc.field(FIELD_SERIALIZATION_TYPE, SERIALIZATION_TYPE_COLLECTION);
doc.field(FIELD_COLLECTION_TYPE, type.name());
return doc;
}
}
ORecordBytes record = new ORecordBytes();
int serType = SERIALIZATION_TYPE_JAVA;
if (value instanceof GeometryProperty<?>) {
GeometryProperty<?> geomProp = (GeometryProperty<?>) value;
// store (runtime) CRS ID (XXX OK as storage is temporary)
doc.field(FIELD_CRS_ID, CRS_IDS.getId(geomProp.getCRSDefinition()));
// extract geometry
value = geomProp.getGeometry();
if (value != null) {
serType = SERIALIZATION_TYPE_GEOM_PROP;
}
else {
return null;
}
}
if (value.getClass().isArray() && value.getClass().getComponentType().equals(byte.class)) {
// direct byte array support
record.fromStream((byte[]) value);
serType = SERIALIZATION_TYPE_BYTEARRAY;
}
if (value instanceof Geometry) {
// serialize geometry as WKB
Geometry geom = (Geometry) value;
Coordinate sample = geom.getCoordinate();
int dimension = (sample != null && !Double.isNaN(sample.z)) ? (3) : (2);
WKBWriter writer = new ExtendedWKBWriter(dimension);
record.fromStream(writer.write(geom));
if (serType != SERIALIZATION_TYPE_GEOM_PROP) {
serType = SERIALIZATION_TYPE_GEOM;
}
}
else {
// object serialization
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
ObjectOutputStream out = new ObjectOutputStream(bytes);
out.writeObject(value);
} catch (IOException e) {
throw new IllegalStateException(
"Could not serialize field value of type " + value.getClass().getName());
}
record.fromStream(bytes.toByteArray());
}
/*
* XXX Class name is set in OGroup.configureDocument, as the class name
* may only bet set after the database was set.
*/
// doc.setClassName(BINARY_WRAPPER_CLASSNAME);
doc.field(BINARY_WRAPPER_FIELD, record);
doc.field(FIELD_SERIALIZATION_TYPE, serType);
return doc;
}
/**
* Convert a value received from the database, e.g. {@link ODocument}s to
* {@link Instance}s, {@link Group}s or unwraps contained values.
*
* @param value the value
* @param parent the parent group
* @param childName the name of the child the value is associated to
* @return the converted object
*/
public static Object convertFromDB(Object value, OGroup parent, QName childName) {
if (value instanceof ODocument) {
ODocument doc = (ODocument) value;
if (doc.containsField(BINARY_WRAPPER_FIELD)
|| doc.containsField(OSerializationHelper.FIELD_SERIALIZATION_TYPE)) {
// extract wrapped ORecordBytes
return OSerializationHelper.deserialize(doc, parent, childName);
}
else {
ChildDefinition<?> child = parent.getDefinition().getChild(childName);
if (child.asProperty() != null) {
return new OInstance((ODocument) value, child.asProperty().getPropertyType(),
parent.getDb(), null); // no data set necessary for
// nested instances
}
else if (child.asGroup() != null) {
return new OGroup((ODocument) value, child.asGroup(), parent.getDb());
}
else {
throw new IllegalStateException("Field " + childName
+ " is associated neither with a property nor a group.");
}
}
}
// TODO also treat collections etc?
// TODO objects that are not supported inside document
if (value instanceof ORecordBytes) {
// XXX should not be reached as every ORecordBytes should be
// contained in a wrapper
// TODO try conversion first?!
// object deserialization
ORecordBytes record = (ORecordBytes) value;
ByteArrayInputStream bytes = new ByteArrayInputStream(record.toStream());
try {
ObjectInputStream in = new ObjectInputStream(bytes) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
Class<?> result = resolved.get(desc.getName());
if (result == null) {
result = OsgiUtils.loadClass(desc.getName(), null);
if (resolved.size() > 200) {
resolved.entrySet().iterator().remove();
}
resolved.put(desc.getName(), result);
}
return result;
}
};
return in.readObject();
} catch (Exception e) {
throw new IllegalStateException("Could not deserialize field value.", e);
}
}
return value;
}
/**
* Deserialize a serialized value wrapped in the given document.
*
* @param doc the document
* @param parent the parent group
* @param childName the name of the child the value is associated to
* @return the deserialized value
*/
public static Object deserialize(ODocument doc, OGroup parent, QName childName) {
int serType = doc.field(FIELD_SERIALIZATION_TYPE);
switch (serType) {
case SERIALIZATION_TYPE_STRING: {
// convert a string value back to its original form
Object val = doc.field(FIELD_STRING_VALUE);
String stringVal = (val == null) ? (null) : (val.toString());
ConvertProxy cp = CONVERTER_IDS.getObject((String) doc.field(FIELD_CONVERT_ID));
if (cp != null) {
return cp.convert(stringVal);
}
return stringVal;
}
case SERIALIZATION_TYPE_COLLECTION: {
// recreate collection
Object val = doc.field(FIELD_VALUES);
Object typeVal = doc.field(FIELD_COLLECTION_TYPE);
CollectionType type = (typeVal != null) ? (CollectionType.valueOf(typeVal.toString()))
: (CollectionType.LIST);
if (val instanceof Collection<?>) {
Collection<?> values = (Collection<?>) val;
Collection<Object> target = createCollection(type);
for (Object element : values) {
Object convElement = convertFromDB(element, parent, childName);
target.add(convElement);
}
return target;
}
}
break;
}
ORecordBytes record = (ORecordBytes) doc.field(BINARY_WRAPPER_FIELD);
Object result;
switch (serType) {
case SERIALIZATION_TYPE_BYTEARRAY:
result = record.toStream();
break;
case SERIALIZATION_TYPE_GEOM:
case SERIALIZATION_TYPE_GEOM_PROP:
ExtendedWKBReader reader = new ExtendedWKBReader();
try {
result = reader.read(record.toStream());
} catch (ParseException e1) {
throw new IllegalStateException("Unable to parse WKB to restore geometry", e1);
}
break;
case SERIALIZATION_TYPE_JAVA:
default:
ByteArrayInputStream bytes = new ByteArrayInputStream(record.toStream());
try {
ObjectInputStream in = new ObjectInputStream(bytes) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
Class<?> result = resolved.get(desc.getName());
if (result == null) {
result = OsgiUtils.loadClass(desc.getName(), null);
if (resolved.size() > 200) {
resolved.entrySet().iterator().remove();
}
resolved.put(desc.getName(), result);
}
if (result == null) {
throw new IllegalStateException(
"Class " + desc.getName() + " not found");
}
return result;
}
};
result = in.readObject();
} catch (Exception e) {
throw new IllegalStateException("Could not deserialize field value.", e);
}
break;
}
if (serType == SERIALIZATION_TYPE_GEOM_PROP) {
// wrap geometry in geometry property
// determine CRS
CRSDefinition crs = null;
Object crsId = doc.field(FIELD_CRS_ID);
if (crsId != null) {
crs = CRS_IDS.getObject(crsId.toString());
}
// create geometry property
GeometryProperty<Geometry> prop = new DefaultGeometryProperty<Geometry>(crs,
(Geometry) result);
return prop;
}
return result;
}
private static Collection<Object> createCollection(CollectionType type) {
switch (type) {
case SET:
return new HashSet<Object>();
case LIST:
default:
return new ArrayList<Object>();
}
}
}