/**
* GRANITE DATA SERVICES
* Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S.
*
* This file is part of the Granite Data Services Platform.
*
* Granite Data Services is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* Granite Data Services is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA, or see <http://www.gnu.org/licenses/>.
*/
package org.granite.hibernate;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentHashMap;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import org.granite.collections.BasicMap;
import org.granite.config.ConvertersConfig;
import org.granite.context.GraniteContext;
import org.granite.logging.Logger;
import org.granite.messaging.amf.io.convert.Converters;
import org.granite.messaging.amf.io.util.ClassGetter;
import org.granite.messaging.amf.io.util.MethodProperty;
import org.granite.messaging.amf.io.util.Property;
import org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer;
import org.granite.messaging.annotations.Include;
import org.granite.messaging.persistence.AbstractExternalizablePersistentCollection;
import org.granite.messaging.persistence.ExternalizablePersistentBag;
import org.granite.messaging.persistence.ExternalizablePersistentList;
import org.granite.messaging.persistence.ExternalizablePersistentMap;
import org.granite.messaging.persistence.ExternalizablePersistentSet;
import org.granite.util.StringUtil;
import org.granite.util.TypeUtil;
import org.granite.util.XMap;
import org.hibernate.Hibernate;
import org.hibernate.annotations.Sort;
import org.hibernate.annotations.SortType;
import org.hibernate.collection.PersistentBag;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.collection.PersistentList;
import org.hibernate.collection.PersistentMap;
import org.hibernate.collection.PersistentSet;
import org.hibernate.collection.PersistentSortedMap;
import org.hibernate.collection.PersistentSortedSet;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
/**
* @author Franck WOLFF
*/
public class HibernateExternalizer extends DefaultExternalizer {
private static final Logger log = Logger.getLogger(HibernateExternalizer.class);
private final ConcurrentHashMap<String, ProxyFactory> proxyFactories = new ConcurrentHashMap<String, ProxyFactory>();
static enum SerializeMetadata {
YES,
NO,
LAZY
}
private SerializeMetadata serializeMetadata = SerializeMetadata.NO;
/**
* Configure this externalizer with the values supplied in granite-config.xml.
*
* <p>The only supported configuration option is 'hibernate-collection-metadata' with
* values in ['no' (default), 'yes' and 'lazy']. By default, collection metadata (key,
* role and snapshot) aren't serialized. If the value of the 'hibernate-collection-metadata'
* node is 'yes', metadata will be always serialized, while the 'lazy' value tells the
* externalizer to serialiaze metadata for uninitialized collections only.
*
* <p>Configuration example (granite-config.xml):
* <pre>
* <granite-config scan="true">
* <externalizers>
* <configuration>
* <hibernate-collection-metadata>lazy</hibernate-collection-metadata>
* </configuration>
* </externalizers>
* </granite-config>
* </pre>
*
* @param properties an XMap instance that contains the configuration node.
*/
@Override
public void configure(XMap properties) {
super.configure(properties);
if (properties != null) {
String collectionmetadata = properties.get("hibernate-collection-metadata");
if (collectionmetadata != null) {
if ("no".equalsIgnoreCase(collectionmetadata))
serializeMetadata = SerializeMetadata.NO;
else if ("yes".equalsIgnoreCase(collectionmetadata))
serializeMetadata = SerializeMetadata.YES;
else if ("lazy".equalsIgnoreCase(collectionmetadata))
serializeMetadata = SerializeMetadata.LAZY;
else
throw new RuntimeException("Illegal value for the 'hibernate-collection-metadata' option: " + collectionmetadata);
}
}
}
@Override
public Object newInstance(String type, ObjectInput in)
throws IOException, ClassNotFoundException, InstantiationException, InvocationTargetException, IllegalAccessException {
// If type is not an entity (@Embeddable for example), we don't read initialized/detachedState
// and we fall back to DefaultExternalizer behavior.
Class<?> clazz = TypeUtil.forName(type);
if (!isRegularEntity(clazz))
return super.newInstance(type, in);
// Read initialized flag.
boolean initialized = ((Boolean)in.readObject()).booleanValue();
// Read detachedState.
String detachedState = (String)in.readObject();
// New or initialized entity.
if (initialized)
return super.newInstance(type, in);
// Actual proxy instantiation is deferred in order to keep consistent order in
// stored objects list (see AMF3Deserializer).
return newProxyInstantiator(proxyFactories, detachedState);
}
protected Object newProxyInstantiator(ConcurrentHashMap<String, ProxyFactory> proxyFactories, String detachedState) {
return new HibernateProxyInstantiator(proxyFactories, detachedState);
}
@Override
public void readExternal(Object o, ObjectInput in) throws IOException, ClassNotFoundException, IllegalAccessException {
// Skip unserialized fields for proxies (only read id).
if (o instanceof HibernateProxyInstantiator) {
log.debug("Reading Hibernate Proxy...");
((HibernateProxyInstantiator)o).readId(in);
}
// @Embeddable or others...
else if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) {
log.debug("Delegating non regular entity reading to DefaultExternalizer...");
super.readExternal(o, in);
}
// Regular @Entity or @MappedSuperclass
else {
ConvertersConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
Converters converters = config.getConverters();
ClassGetter classGetter = config.getClassGetter();
Class<?> oClass = classGetter.getClass(o);
ParameterizedType[] declaringTypes = TypeUtil.getDeclaringTypes(oClass);
List<Property> fields = findOrderedFields(oClass, false);
log.debug("Reading entity %s with fields %s", oClass.getName(), fields);
for (Property field : fields) {
Object value = in.readObject();
if (!(field instanceof MethodProperty && field.isAnnotationPresent(Include.class, true))) {
if (value instanceof AbstractExternalizablePersistentCollection)
value = newHibernateCollection((AbstractExternalizablePersistentCollection)value, field);
else if (!(value instanceof HibernateProxy)) {
Type targetType = TypeUtil.resolveTypeVariable(field.getType(), field.getDeclaringClass(), declaringTypes);
value = converters.convert(value, targetType);
}
field.setValue(o, value, false);
}
}
}
}
protected PersistentCollection newHibernateCollection(AbstractExternalizablePersistentCollection value, Property field) {
final Type target = field.getType();
final boolean initialized = value.isInitialized();
final String metadata = value.getMetadata();
final boolean dirty = value.isDirty();
final boolean sorted = (
SortedSet.class.isAssignableFrom(TypeUtil.classOfType(target)) ||
SortedMap.class.isAssignableFrom(TypeUtil.classOfType(target))
);
Comparator<?> comparator = null;
if (sorted && field.isAnnotationPresent(Sort.class)) {
Sort sort = field.getAnnotation(Sort.class);
if (sort.type() == SortType.COMPARATOR) {
try {
comparator = TypeUtil.newInstance(sort.comparator(), Comparator.class);
} catch (Exception e) {
throw new RuntimeException("Could not create instance of Comparator: " + sort.comparator());
}
}
}
PersistentCollection coll = null;
if (value instanceof ExternalizablePersistentSet) {
if (initialized) {
Set<?> set = ((ExternalizablePersistentSet)value).getContentAsSet(target, comparator);
coll = (sorted ? new PersistentSortedSet(null, (SortedSet<?>)set) : new PersistentSet(null, set));
}
else
coll = (sorted ? new PersistentSortedSet() : new PersistentSet());
}
else if (value instanceof ExternalizablePersistentBag) {
if (initialized) {
List<?> bag = ((ExternalizablePersistentBag)value).getContentAsList(target);
coll = new PersistentBag(null, bag);
}
else
coll = new PersistentBag();
}
else if (value instanceof ExternalizablePersistentList) {
if (initialized) {
List<?> list = ((ExternalizablePersistentList)value).getContentAsList(target);
coll = new PersistentList(null, list);
}
else
coll = new PersistentList();
}
else if (value instanceof ExternalizablePersistentMap) {
if (initialized) {
Map<?, ?> map = ((ExternalizablePersistentMap)value).getContentAsMap(target, comparator);
coll = (sorted ? new PersistentSortedMap(null, (SortedMap<?, ?>)map) : new PersistentMap(null, map));
}
else
coll = (sorted ? new PersistentSortedMap() : new PersistentMap());
}
else
throw new RuntimeException("Illegal externalizable persitent class: " + value);
if (metadata != null && serializeMetadata != SerializeMetadata.NO && (serializeMetadata == SerializeMetadata.YES || !initialized)) {
String[] toks = metadata.split(":", 3);
if (toks.length != 3)
throw new RuntimeException("Invalid collection metadata: " + metadata);
Serializable key = deserializeSerializable(StringUtil.hexStringToBytes(toks[0]));
Serializable snapshot = deserializeSerializable(StringUtil.hexStringToBytes(toks[1]));
String role = toks[2];
coll.setSnapshot(key, role, snapshot);
}
if (initialized && dirty)
coll.dirty();
return coll;
}
@Override
public void writeExternal(Object o, ObjectOutput out) throws IOException, IllegalAccessException {
ClassGetter classGetter = ((ConvertersConfig)GraniteContext.getCurrentInstance().getGraniteConfig()).getClassGetter();
Class<?> oClass = classGetter.getClass(o);
String detachedState = null;
if (o instanceof HibernateProxy) {
HibernateProxy proxy = (HibernateProxy)o;
detachedState = getProxyDetachedState(proxy);
// Only write initialized flag & detachedState & entity id if proxy is uninitialized.
if (proxy.getHibernateLazyInitializer().isUninitialized()) {
Serializable id = proxy.getHibernateLazyInitializer().getIdentifier();
log.debug("Writing uninitialized HibernateProxy %s with id %s", detachedState, id);
// Write initialized flag.
out.writeObject(Boolean.FALSE);
// Write detachedState.
out.writeObject(detachedState);
// Write entity id.
out.writeObject(id);
return;
}
// Proxy is initialized, get the underlying persistent object.
log.debug("Writing initialized HibernateProxy...");
o = proxy.getHibernateLazyInitializer().getImplementation();
}
if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) { // @Embeddable or others...
log.debug("Delegating non regular entity writing to DefaultExternalizer...");
super.writeExternal(o, out);
}
else {
if (isRegularEntity(o.getClass())) {
// Write initialized flag.
out.writeObject(Boolean.TRUE);
// Write detachedState.
out.writeObject(detachedState);
}
// Externalize entity fields.
List<Property> fields = findOrderedFields(oClass, false);
log.debug("Writing entity %s with fields %s", o.getClass().getName(), fields);
for (Property field : fields) {
Object value = field.getValue(o);
// Persistent collections.
if (value instanceof PersistentCollection)
value = newExternalizableCollection((PersistentCollection)value);
// Transient maps.
else if (value instanceof Map<?, ?>)
value = BasicMap.newInstance((Map<?, ?>)value);
if (isValueIgnored(value))
out.writeObject(null);
else
out.writeObject(value);
}
}
}
protected AbstractExternalizablePersistentCollection newExternalizableCollection(PersistentCollection value) {
final boolean initialized = Hibernate.isInitialized(value);
final boolean dirty = value.isDirty();
AbstractExternalizablePersistentCollection coll = null;
if (value instanceof PersistentSet)
coll = new ExternalizablePersistentSet(initialized ? (Set<?>)value : null, initialized, dirty);
else if (value instanceof PersistentList)
coll = new ExternalizablePersistentList(initialized ? (List<?>)value : null, initialized, dirty);
else if (value instanceof PersistentBag)
coll = new ExternalizablePersistentBag(initialized ? (List<?>)value : null, initialized, dirty);
else if (value instanceof PersistentMap)
coll = new ExternalizablePersistentMap(initialized ? (Map<?, ?>)value : null, initialized, dirty);
else
throw new UnsupportedOperationException("Unsupported Hibernate collection type: " + value);
if (serializeMetadata != SerializeMetadata.NO && (serializeMetadata == SerializeMetadata.YES || !initialized) && value.getRole() != null) {
char[] hexKey = StringUtil.bytesToHexChars(serializeSerializable(value.getKey()));
char[] hexSnapshot = StringUtil.bytesToHexChars(serializeSerializable(value.getStoredSnapshot()));
String metadata = new StringBuilder(hexKey.length + 1 + hexSnapshot.length + 1 + value.getRole().length())
.append(hexKey).append(':')
.append(hexSnapshot).append(':')
.append(value.getRole())
.toString();
coll.setMetadata(metadata);
}
return coll;
}
@Override
public int accept(Class<?> clazz) {
return (
clazz.isAnnotationPresent(Entity.class) ||
clazz.isAnnotationPresent(MappedSuperclass.class) ||
clazz.isAnnotationPresent(Embeddable.class)
) ? 1 : -1;
}
protected String getProxyDetachedState(HibernateProxy proxy) {
LazyInitializer initializer = proxy.getHibernateLazyInitializer();
StringBuilder sb = new StringBuilder();
sb.append(initializer.getClass().getName())
.append(':');
if (initializer.getPersistentClass() != null)
sb.append(initializer.getPersistentClass().getName());
sb.append(':');
if (initializer.getEntityName() != null)
sb.append(initializer.getEntityName());
return sb.toString();
}
protected boolean isRegularEntity(Class<?> clazz) {
return clazz.isAnnotationPresent(Entity.class) || clazz.isAnnotationPresent(MappedSuperclass.class);
}
protected boolean isEmbeddable(Class<?> clazz) {
return clazz.isAnnotationPresent(Embeddable.class);
}
protected byte[] serializeSerializable(Serializable o) {
if (o == null)
return BYTES_0;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("Could not serialize: " + o);
}
}
protected Serializable deserializeSerializable(byte[] data) {
if (data.length == 0)
return null;
try {
ByteArrayInputStream baos = new ByteArrayInputStream(data);
ObjectInputStream oos = new ObjectInputStream(baos);
return (Serializable)oos.readObject();
} catch (Exception e) {
throw new RuntimeException("Could not deserialize: " + data);
}
}
}