/** * 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.datanucleus; 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.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import javax.jdo.annotations.EmbeddedOnly; import javax.jdo.annotations.Extension; import javax.jdo.spi.Detachable; import javax.jdo.spi.PersistenceCapable; import javax.jdo.spi.StateManager; import javax.persistence.Version; 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.ExternalizablePersistentList; import org.granite.messaging.persistence.ExternalizablePersistentMap; import org.granite.messaging.persistence.ExternalizablePersistentSet; import org.granite.util.Reflections; import org.granite.util.StringUtil; import org.granite.util.TypeUtil; /** * @author Stephen MORE * @author William DRAI */ @SuppressWarnings("unchecked") public class DataNucleusExternalizer extends DefaultExternalizer { private static final Logger log = Logger.getLogger(DataNucleusExternalizer.class); private static final Integer NULL_ID = Integer.valueOf(0); private static boolean jpaEnabled; private static Class<? extends Annotation> entityAnnotation; private static Class<? extends Annotation> mappedSuperClassAnnotation; private static Class<? extends Annotation> embeddableAnnotation; private static Class<? extends Annotation> idClassAnnotation; static { try { ClassLoader cl = DataNucleusExternalizer.class.getClassLoader(); entityAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.Entity"); mappedSuperClassAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.MappedSuperclass"); embeddableAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.Embeddable"); idClassAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.IdClass"); jpaEnabled = true; } catch (Exception e) { // JPA not present entityAnnotation = null; mappedSuperClassAnnotation = null; embeddableAnnotation = null; idClassAnnotation = null; jpaEnabled = false; } } @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 entity. if (initialized && detachedState == null) return super.newInstance(type, in); // Pseudo-proxy (uninitialized entity). if (!initialized) { Object id = in.readObject(); if (id != null && jpaEnabled) { // Is there something similar for JDO ?? boolean error = !clazz.isAnnotationPresent(idClassAnnotation); if (!error) { Object idClass = clazz.getAnnotation(idClassAnnotation); try { Method m = idClass.getClass().getMethod("value"); error = !id.getClass().equals(m.invoke(idClass)); } catch (Exception e) { log.error(e, "Could not get idClass annotation value"); error = true; } } if (error) throw new RuntimeException("Id for DataNucleus pseudo-proxy should be null (" + type + ")"); } return null; } // Existing entity. Object entity = clazz.newInstance(); if (detachedState.length() > 0) { byte[] data = StringUtil.hexStringToBytes(detachedState); deserializeDetachedState((Detachable)entity, data); } return entity; } @Override public void readExternal(Object o, ObjectInput in) throws IOException, ClassNotFoundException, IllegalAccessException { 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); Object[] detachedState = getDetachedState((Detachable)o); List<Property> fields = findOrderedFields(oClass, detachedState != null); log.debug("Reading entity %s with fields %s", oClass.getName(), fields); for (Property field : fields) { if (field.getName().equals("jdoDetachedState")) continue; Object value = in.readObject(); if (!(field instanceof MethodProperty && field.isAnnotationPresent(Include.class, true))) { // (Un)Initialized collections/maps. if (value instanceof AbstractExternalizablePersistentCollection) value = newCollection((AbstractExternalizablePersistentCollection)value, field); else { Type targetType = TypeUtil.resolveTypeVariable(field.getType(), field.getDeclaringClass(), declaringTypes); value = converters.convert(value, targetType); } field.setValue(o, value, false); } } } } protected Object newCollection(AbstractExternalizablePersistentCollection value, Property field) { final Type target = field.getType(); final boolean initialized = value.isInitialized(); // final boolean dirty = value.isDirty(); final Object[] content = value.getContent(); final boolean sorted = ( SortedSet.class.isAssignableFrom(TypeUtil.classOfType(target)) || SortedMap.class.isAssignableFrom(TypeUtil.classOfType(target)) ); Object coll = null; if (value instanceof ExternalizablePersistentSet) { if (initialized) { if (content != null) coll = ((ExternalizablePersistentSet)value).getContentAsSet(target); } else coll = (sorted ? new TreeSet<Object>() : new HashSet<Object>()); } else if (value instanceof ExternalizablePersistentList) { if (initialized) { if (content != null) coll = ((ExternalizablePersistentList)value).getContentAsList(target); } else coll = new ArrayList<Object>(); } else if (value instanceof ExternalizablePersistentMap) { if (initialized) { if (content != null) coll = ((ExternalizablePersistentMap)value).getContentAsMap(target); } else coll = (sorted ? new TreeMap<Object, Object>() : new HashMap<Object, Object>()); } else { throw new RuntimeException("Illegal externalizable persitent class: " + value); } 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); if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) { // @Embeddable or others... log.debug("Delegating non regular entity writing to DefaultExternalizer..."); super.writeExternal(o, out); } else { Detachable pco = (Detachable)o; preSerialize((PersistenceCapable)pco); Object[] detachedState = getDetachedState(pco); if (isRegularEntity(o.getClass())) { // Pseudo-proxy created for uninitialized entities (see below). if (detachedState != null && detachedState[0] == NULL_ID) { // Write initialized flag. out.writeObject(Boolean.FALSE); // Write detached state. out.writeObject(null); // Write id. out.writeObject(null); return; } // Write initialized flag. out.writeObject(Boolean.TRUE); if (detachedState != null) { // Write detached state as a String, in the form of an hex representation // of the serialized detached state. Object version = getVersion(pco); if (version != null) detachedState[1] = version; byte[] binDetachedState = serializeDetachedState(detachedState); char[] hexDetachedState = StringUtil.bytesToHexChars(binDetachedState); out.writeObject(new String(hexDetachedState)); } else out.writeObject(null); } // Externalize entity fields. List<Property> fields = findOrderedFields(oClass); Map<String, Boolean> loadedState = getLoadedState(detachedState, oClass); log.debug("Writing entity %s with fields %s", o.getClass().getName(), fields); for (Property field : fields) { if (field.getName().equals("jdoDetachedState")) continue; Object value = field.getValue(o); if (isValueIgnored(value)) { out.writeObject(null); continue; } // Uninitialized associations. if (loadedState.containsKey(field.getName()) && !loadedState.get(field.getName())) { Class<?> fieldClass = TypeUtil.classOfType(field.getType()); // Create a "pseudo-proxy" for uninitialized entities: detached state is set to "0" (uninitialized flag). if (Detachable.class.isAssignableFrom(fieldClass)) { try { value = fieldClass.newInstance(); } catch (Exception e) { throw new RuntimeException("Could not create DataNucleus pseudo-proxy for: " + field, e); } setDetachedState((Detachable)value, new Object[] { NULL_ID, null, null, null }); } // Create pseudo-proxy for collections (set or list). else if (Collection.class.isAssignableFrom(fieldClass)) { if (Set.class.isAssignableFrom(fieldClass)) value = new ExternalizablePersistentSet((Set<?>)null, false, false); else value = new ExternalizablePersistentList((List<?>)null, false, false); } // Create pseudo-proxy for maps. else if (Map.class.isAssignableFrom(fieldClass)) { value = new ExternalizablePersistentMap((Map<?, ?>)null, false, false); } } // Initialized collections. else if (value instanceof Set<?>) { value = new ExternalizablePersistentSet(((Set<?>)value).toArray(), true, false); } else if (value instanceof List<?>) { value = new ExternalizablePersistentList(((List<?>)value).toArray(), true, false); } else if (value instanceof Map<?, ?>) { value = new ExternalizablePersistentMap((Map<?, ?>)null, true, false); ((ExternalizablePersistentMap)value).setContentFromMap((Map<?, ?>)value); } out.writeObject(value); } } } @Override public int accept(Class<?> clazz) { return ( clazz.isAnnotationPresent(entityAnnotation) || clazz.isAnnotationPresent(mappedSuperClassAnnotation) || clazz.isAnnotationPresent(embeddableAnnotation) || clazz.isAnnotationPresent(javax.jdo.annotations.PersistenceCapable.class) ) ? 1 : -1; } protected boolean isRegularEntity(Class<?> clazz) { if (jpaEnabled) { return ((PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && !clazz.isAnnotationPresent(EmbeddedOnly.class)) || clazz.isAnnotationPresent(entityAnnotation) || clazz.isAnnotationPresent(mappedSuperClassAnnotation)) && !(clazz.isAnnotationPresent(embeddableAnnotation)); } return PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && !clazz.isAnnotationPresent(EmbeddedOnly.class); } protected boolean isEmbeddable(Class<?> clazz) { if (jpaEnabled) { return ((PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && clazz.isAnnotationPresent(EmbeddedOnly.class)) || clazz.isAnnotationPresent(embeddableAnnotation)) && !(clazz.isAnnotationPresent(entityAnnotation) || clazz.isAnnotationPresent(mappedSuperClassAnnotation)); } return PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && clazz.isAnnotationPresent(EmbeddedOnly.class); } @Override public List<Property> findOrderedFields(final Class<?> clazz, boolean returnSettersWhenAvailable) { List<Property> orderedFields = super.findOrderedFields(clazz, returnSettersWhenAvailable); if (clazz.isAnnotationPresent(EmbeddedOnly.class) || (jpaEnabled && clazz.isAnnotationPresent(embeddableAnnotation))) { Iterator<Property> ifield = orderedFields.iterator(); while (ifield.hasNext()) { Property field = ifield.next(); if (field.getName().equals("jdoDetachedState")) ifield.remove(); } } return orderedFields; } private static void preSerialize(PersistenceCapable o) { try { Class<?> baseClass = o.getClass(); while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass())) { baseClass = baseClass.getSuperclass(); } Field f = baseClass.getDeclaredField("jdoStateManager"); f.setAccessible(true); StateManager sm = (StateManager)f.get(o); if (sm != null) { setDetachedState((Detachable)o, null); sm.preSerialize(o); } } catch (Exception e) { throw new RuntimeException("Cannot access jdoDetachedState for detached object", e); } } private static Object[] getDetachedState(javax.jdo.spi.Detachable o) { try { Class<?> baseClass = o.getClass(); while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass())) baseClass = baseClass.getSuperclass(); Field f = baseClass.getDeclaredField("jdoDetachedState"); f.setAccessible(true); return (Object[])f.get(o); } catch (Exception e) { throw new RuntimeException("Cannot access jdoDetachedState for detached object", e); } } private static void setDetachedState(javax.jdo.spi.Detachable o, Object[] detachedState) { try { Class<?> baseClass = o.getClass(); while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass())) baseClass = baseClass.getSuperclass(); Field f = baseClass.getDeclaredField("jdoDetachedState"); f.setAccessible(true); f.set(o, detachedState); } catch (Exception e) { throw new RuntimeException("Cannot access jdoDetachedState for detached object", e); } } static Map<String, Boolean> getLoadedState(Detachable pc, Class<?> clazz) { return getLoadedState(getDetachedState(pc), clazz); } static Map<String, Boolean> getLoadedState(Object[] detachedState, Class<?> clazz) { try { BitSet loaded = detachedState != null ? (BitSet)detachedState[2] : null; List<String> fieldNames = new ArrayList<String>(); for (Class<?> c = clazz; c != null && PersistenceCapable.class.isAssignableFrom(c); c = c.getSuperclass()) { Field pcFieldNames = c.getDeclaredField("jdoFieldNames"); pcFieldNames.setAccessible(true); fieldNames.addAll(0, Arrays.asList((String[])pcFieldNames.get(null))); } Map<String, Boolean> loadedState = new HashMap<String, Boolean>(); for (int i = 0; i < fieldNames.size(); i++) loadedState.put(fieldNames.get(i), (loaded != null && loaded.size() > i ? loaded.get(i) : true)); return loadedState; } catch (Exception e) { throw new RuntimeException("Could not get loaded state for: " + detachedState); } } protected byte[] serializeDetachedState(Object[] detachedState) { try { // Force version ByteArrayOutputStream baos = new ByteArrayOutputStream(256); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(detachedState); return baos.toByteArray(); } catch (Exception e) { throw new RuntimeException("Could not serialize detached state for: " + detachedState); } } protected void deserializeDetachedState(Detachable pc, byte[] data) { try { ByteArrayInputStream baos = new ByteArrayInputStream(data); ObjectInputStream oos = new ObjectInputStream(baos); Object[] state = (Object[])oos.readObject(); setDetachedState(pc, state); } catch (Exception e) { throw new RuntimeException("Could not deserialize detached state for: " + data); } } protected static Object getVersion(Object entity) { Class<?> entityClass = entity.getClass(); if (jpaEnabled && entityClass.isAnnotationPresent(entityAnnotation)) { for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass()) { for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(Version.class)) { return Reflections.invokeAndWrap(method, entity); } } } for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass()) { for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Version.class)) { if (!field.isAccessible()) field.setAccessible(true); return Reflections.getAndWrap(field, entity); } } } return null; } else if (!jpaEnabled && entity instanceof PersistenceCapable) { if (entityClass.isAnnotationPresent(javax.jdo.annotations.Version.class)) { javax.jdo.annotations.Version version = entityClass.getAnnotation(javax.jdo.annotations.Version.class); for (Extension extension : version.extensions()) { if (extension.vendorName().equals("datanucleus") && extension.key().equals("field-name")) { String versionFieldName = extension.value(); try { Method versionGetter = entityClass.getMethod("get" + versionFieldName.substring(0, 1).toUpperCase() + versionFieldName.substring(1)); return Reflections.invokeAndWrap(versionGetter, entity); } catch (NoSuchMethodException e) { for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass()) { for (Field field : clazz.getDeclaredFields()) { if (field.getName().equals(versionFieldName)) { if (!field.isAccessible()) field.setAccessible(true); return Reflections.getAndWrap(field, entity); } } } } } } } } return null; } }