001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004    
005      This file is part of Granite Data Services.
006    
007      Granite Data Services is free software; you can redistribute it and/or modify
008      it under the terms of the GNU Library General Public License as published by
009      the Free Software Foundation; either version 2 of the License, or (at your
010      option) any later version.
011    
012      Granite Data Services is distributed in the hope that it will be useful, but
013      WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014      FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015      for more details.
016    
017      You should have received a copy of the GNU Library General Public License
018      along with this library; if not, see <http://www.gnu.org/licenses/>.
019    */
020    
021    package org.granite.datanucleus;
022    
023    import java.io.ByteArrayInputStream;
024    import java.io.ByteArrayOutputStream;
025    import java.io.IOException;
026    import java.io.ObjectInput;
027    import java.io.ObjectInputStream;
028    import java.io.ObjectOutput;
029    import java.io.ObjectOutputStream;
030    import java.lang.annotation.Annotation;
031    import java.lang.reflect.Field;
032    import java.lang.reflect.InvocationTargetException;
033    import java.lang.reflect.Method;
034    import java.lang.reflect.ParameterizedType;
035    import java.lang.reflect.Type;
036    import java.util.ArrayList;
037    import java.util.Arrays;
038    import java.util.BitSet;
039    import java.util.Collection;
040    import java.util.HashMap;
041    import java.util.HashSet;
042    import java.util.Iterator;
043    import java.util.List;
044    import java.util.Map;
045    import java.util.Set;
046    import java.util.SortedMap;
047    import java.util.SortedSet;
048    import java.util.TreeMap;
049    import java.util.TreeSet;
050    
051    import javax.jdo.annotations.EmbeddedOnly;
052    import javax.jdo.annotations.Extension;
053    import javax.jdo.spi.Detachable;
054    import javax.jdo.spi.PersistenceCapable;
055    import javax.jdo.spi.StateManager;
056    import javax.persistence.Version;
057    
058    import org.granite.config.GraniteConfig;
059    import org.granite.context.GraniteContext;
060    import org.granite.logging.Logger;
061    import org.granite.messaging.amf.io.convert.Converters;
062    import org.granite.messaging.amf.io.util.ClassGetter;
063    import org.granite.messaging.amf.io.util.MethodProperty;
064    import org.granite.messaging.amf.io.util.Property;
065    import org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer;
066    import org.granite.messaging.amf.io.util.externalizer.annotation.ExternalizedProperty;
067    import org.granite.messaging.persistence.AbstractExternalizablePersistentCollection;
068    import org.granite.messaging.persistence.ExternalizablePersistentList;
069    import org.granite.messaging.persistence.ExternalizablePersistentMap;
070    import org.granite.messaging.persistence.ExternalizablePersistentSet;
071    import org.granite.util.TypeUtil;
072    import org.granite.util.Reflections;
073    import org.granite.util.StringUtil;
074    
075    
076    /**
077     * @author Stephen MORE
078     * @author William DRAI
079     */
080    @SuppressWarnings("unchecked")
081    public class DataNucleusExternalizer extends DefaultExternalizer {
082    
083            private static final Logger log = Logger.getLogger(DataNucleusExternalizer.class);
084            
085            private static final Integer NULL_ID = Integer.valueOf(0);
086            
087            private static boolean jpaEnabled;
088            private static Class<? extends Annotation> entityAnnotation;
089            private static Class<? extends Annotation> mappedSuperClassAnnotation;
090            private static Class<? extends Annotation> embeddableAnnotation;
091            private static Class<? extends Annotation> idClassAnnotation;
092            static {
093                    try {
094                            ClassLoader cl = DataNucleusExternalizer.class.getClassLoader();
095                            entityAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.Entity");
096                            mappedSuperClassAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.MappedSuperclass");
097                            embeddableAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.Embeddable");
098                            idClassAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.IdClass");
099                            jpaEnabled = true;
100                    }
101                    catch (Exception e) {
102                            // JPA not present
103                            entityAnnotation = null;
104                            mappedSuperClassAnnotation = null;
105                            embeddableAnnotation = null;
106                            idClassAnnotation = null;
107                            jpaEnabled = false;
108                    }
109            }
110            
111    
112        @Override
113        public Object newInstance(String type, ObjectInput in)
114            throws IOException, ClassNotFoundException, InstantiationException, InvocationTargetException, IllegalAccessException {
115    
116            // If type is not an entity (@Embeddable for example), we don't read initialized/detachedState
117            // and we fall back to DefaultExternalizer behavior.
118            Class<?> clazz = TypeUtil.forName(type);
119            if (!isRegularEntity(clazz))
120                return super.newInstance(type, in);
121    
122            // Read initialized flag.
123            boolean initialized = ((Boolean)in.readObject()).booleanValue();
124    
125            // Read detachedState.
126            String detachedState = (String)in.readObject();
127            
128            // New entity.
129            if (initialized && detachedState == null)
130                    return super.newInstance(type, in);
131            
132            // Pseudo-proxy (uninitialized entity).
133            if (!initialized) {
134                    Object id = in.readObject();
135                    if (id != null && jpaEnabled) {
136                            // Is there something similar for JDO ??
137                            boolean error = !clazz.isAnnotationPresent(idClassAnnotation);
138                            if (!error) {
139                                    Object idClass = clazz.getAnnotation(idClassAnnotation);
140                                    try {
141                                            Method m = idClass.getClass().getMethod("value");
142                                            error = !id.getClass().equals(m.invoke(idClass));
143                                    }
144                                    catch (Exception e) {
145                                            log.error(e, "Could not get idClass annotation value");
146                                            error = true;
147                                    }
148                            }
149                            if (error)
150                                    throw new RuntimeException("Id for DataNucleus pseudo-proxy should be null (" + type + ")");
151                    }
152                    return null;
153            }
154            
155            // Existing entity.
156                    Object entity = clazz.newInstance();
157                    if (detachedState.length() > 0) {
158                    byte[] data = StringUtil.hexStringToBytes(detachedState);
159                            deserializeDetachedState((Detachable)entity, data);
160                    }
161                    return entity;
162        }
163    
164        @Override
165        public void readExternal(Object o, ObjectInput in) throws IOException, ClassNotFoundException, IllegalAccessException {
166    
167            if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) {
168                    log.debug("Delegating non regular entity reading to DefaultExternalizer...");
169                super.readExternal(o, in);
170            }
171            // Regular @Entity or @MappedSuperclass
172            else {
173                GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
174    
175                Converters converters = config.getConverters();
176                ClassGetter classGetter = config.getClassGetter();
177                Class<?> oClass = classGetter.getClass(o);
178                ParameterizedType[] declaringTypes = TypeUtil.getDeclaringTypes(oClass);
179                Object[] detachedState = getDetachedState((Detachable)o);
180    
181                List<Property> fields = findOrderedFields(oClass, detachedState != null);
182                log.debug("Reading entity %s with fields %s", oClass.getName(), fields);
183                for (Property field : fields) {
184                    if (field.getName().equals("jdoDetachedState"))
185                            continue;
186                    
187                    Object value = in.readObject();
188                    
189                    if (!(field instanceof MethodProperty && field.isAnnotationPresent(ExternalizedProperty.class, true))) {
190                            
191                            // (Un)Initialized collections/maps.
192                            if (value instanceof AbstractExternalizablePersistentCollection)
193                                    value = newCollection((AbstractExternalizablePersistentCollection)value, field);
194                        else {
195                            Type targetType = TypeUtil.resolveTypeVariable(field.getType(), field.getDeclaringClass(), declaringTypes);
196                                    value = converters.convert(value, targetType);
197                        }
198                        
199                            field.setProperty(o, value, false);
200                    }
201                }
202            }
203        }
204        
205        protected Object newCollection(AbstractExternalizablePersistentCollection value, Property field) {
206            final Type target = field.getType();
207            final boolean initialized = value.isInitialized();
208                    // final boolean dirty = value.isDirty();
209                    final Object[] content = value.getContent();
210            final boolean sorted = (
211                    SortedSet.class.isAssignableFrom(TypeUtil.classOfType(target)) ||
212                    SortedMap.class.isAssignableFrom(TypeUtil.classOfType(target))
213            );
214            
215                    Object coll = null;
216                    if (value instanceof ExternalizablePersistentSet) {
217                    if (initialized) {
218                    if (content != null)
219                            coll = ((ExternalizablePersistentSet)value).getContentAsSet(target);
220                }
221                    else
222                    coll = (sorted ? new TreeSet<Object>() : new HashSet<Object>());
223            }
224                    else if (value instanceof ExternalizablePersistentList) {
225                    if (initialized) {
226                        if (content != null)
227                            coll = ((ExternalizablePersistentList)value).getContentAsList(target);
228                    }
229                    else
230                        coll = new ArrayList<Object>();
231                    }
232                    else if (value instanceof ExternalizablePersistentMap) {
233                    if (initialized) {
234                        if (content != null)
235                            coll = ((ExternalizablePersistentMap)value).getContentAsMap(target);
236                    }
237                    else
238                        coll = (sorted ? new TreeMap<Object, Object>() : new HashMap<Object, Object>());
239                    }
240                    else {
241                            throw new RuntimeException("Illegal externalizable persitent class: " + value);
242                    }
243            
244            return coll;
245        }
246    
247        @Override
248        public void writeExternal(Object o, ObjectOutput out) throws IOException, IllegalAccessException {
249    
250            ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter();
251            Class<?> oClass = classGetter.getClass(o);
252    
253            if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) { // @Embeddable or others...
254                    log.debug("Delegating non regular entity writing to DefaultExternalizer...");
255                super.writeExternal(o, out);
256            }
257            else {
258                    Detachable pco = (Detachable)o;
259                    preSerialize((PersistenceCapable)pco);
260                    Object[] detachedState = getDetachedState(pco);
261                    
262                    if (isRegularEntity(o.getClass())) {            
263                            // Pseudo-proxy created for uninitialized entities (see below).
264                            if (detachedState != null && detachedState[0] == NULL_ID) {
265                            // Write initialized flag.
266                            out.writeObject(Boolean.FALSE);
267                            // Write detached state.
268                                    out.writeObject(null);
269                                    // Write id.
270                                    out.writeObject(null);
271                                    return;
272                            }
273            
274                            // Write initialized flag.
275                            out.writeObject(Boolean.TRUE);
276                            
277                            if (detachedState != null) {
278                            // Write detached state as a String, in the form of an hex representation
279                            // of the serialized detached state.
280                            Object version = getVersion(pco);
281                            if (version != null)
282                                    detachedState[1] = version;
283                                    byte[] binDetachedState = serializeDetachedState(detachedState);
284                                    char[] hexDetachedState = StringUtil.bytesToHexChars(binDetachedState);
285                                out.writeObject(new String(hexDetachedState));
286                            }
287                            else
288                                    out.writeObject(null);
289                    }
290    
291                // Externalize entity fields.
292                List<Property> fields = findOrderedFields(oClass);
293                    Map<String, Boolean> loadedState = getLoadedState(detachedState, oClass);
294                log.debug("Writing entity %s with fields %s", o.getClass().getName(), fields);
295                for (Property field : fields) {
296                    if (field.getName().equals("jdoDetachedState"))
297                            continue;
298                    
299                    Object value = field.getProperty(o);
300                    if (isValueIgnored(value)) {
301                            out.writeObject(null);
302                            continue;
303                    }
304                    
305                    // Uninitialized associations.
306                    if (loadedState.containsKey(field.getName()) && !loadedState.get(field.getName())) {
307                            Class<?> fieldClass = TypeUtil.classOfType(field.getType());
308                                    
309                            // Create a "pseudo-proxy" for uninitialized entities: detached state is set to "0" (uninitialized flag).
310                            if (Detachable.class.isAssignableFrom(fieldClass)) {
311                                    try {
312                                            value = fieldClass.newInstance();
313                                    } catch (Exception e) {
314                                            throw new RuntimeException("Could not create DataNucleus pseudo-proxy for: " + field, e);
315                                    }
316                                    setDetachedState((Detachable)value, new Object[] { NULL_ID, null, null, null });
317                            }
318                            // Create pseudo-proxy for collections (set or list).
319                            else if (Collection.class.isAssignableFrom(fieldClass)) {
320                                    if (Set.class.isAssignableFrom(fieldClass))
321                                            value = new ExternalizablePersistentSet((Set<?>)null, false, false);
322                                    else
323                                            value = new ExternalizablePersistentList((List<?>)null, false, false);
324                            }
325                            // Create pseudo-proxy for maps.
326                            else if (Map.class.isAssignableFrom(fieldClass)) {
327                                    value = new ExternalizablePersistentMap((Map<?, ?>)null, false, false);
328                            }
329                    }
330                    
331                    // Initialized collections.
332                    else if (value instanceof Set<?>) {
333                            value = new ExternalizablePersistentSet(((Set<?>)value).toArray(), true, false);
334                    }
335                    else if (value instanceof List<?>) {
336                            value = new ExternalizablePersistentList(((List<?>)value).toArray(), true, false);
337                    }
338                    else if (value instanceof Map<?, ?>) {
339                            value = new ExternalizablePersistentMap((Map<?, ?>)null, true, false);
340                            ((ExternalizablePersistentMap)value).setContentFromMap((Map<?, ?>)value);
341                    }
342                    out.writeObject(value);
343                }
344            }
345        }
346    
347        @Override
348        public int accept(Class<?> clazz) {
349            return (
350                clazz.isAnnotationPresent(entityAnnotation) ||
351                clazz.isAnnotationPresent(mappedSuperClassAnnotation) ||
352                clazz.isAnnotationPresent(embeddableAnnotation) ||
353                clazz.isAnnotationPresent(javax.jdo.annotations.PersistenceCapable.class)
354            ) ? 1 : -1;
355        }
356    
357        protected boolean isRegularEntity(Class<?> clazz) {
358            if (jpaEnabled) {
359                    return ((PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && !clazz.isAnnotationPresent(EmbeddedOnly.class)) 
360                            || clazz.isAnnotationPresent(entityAnnotation) || clazz.isAnnotationPresent(mappedSuperClassAnnotation))
361                            && !(clazz.isAnnotationPresent(embeddableAnnotation));
362            }
363            return PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && !clazz.isAnnotationPresent(EmbeddedOnly.class);
364        }
365        
366        protected boolean isEmbeddable(Class<?> clazz) {
367            if (jpaEnabled) {
368                    return ((PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && clazz.isAnnotationPresent(EmbeddedOnly.class)) 
369                        || clazz.isAnnotationPresent(embeddableAnnotation))
370                        && !(clazz.isAnnotationPresent(entityAnnotation) || clazz.isAnnotationPresent(mappedSuperClassAnnotation));
371            }
372            return PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && clazz.isAnnotationPresent(EmbeddedOnly.class);
373        }
374    
375        @Override
376        public List<Property> findOrderedFields(final Class<?> clazz, boolean returnSettersWhenAvailable) {
377            List<Property> orderedFields = super.findOrderedFields(clazz, returnSettersWhenAvailable);
378            if (clazz.isAnnotationPresent(EmbeddedOnly.class) || (jpaEnabled && clazz.isAnnotationPresent(embeddableAnnotation))) {
379                    Iterator<Property> ifield = orderedFields.iterator();
380                    while (ifield.hasNext()) {
381                            Property field = ifield.next();
382                            if (field.getName().equals("jdoDetachedState"))
383                                    ifield.remove();
384                    }
385            }
386            return orderedFields;
387        }
388        
389            
390        private static void preSerialize(PersistenceCapable o) {
391            try {
392                    Class<?> baseClass = o.getClass();
393                    while (baseClass.getSuperclass() != Object.class &&
394                               baseClass.getSuperclass() != null &&
395                               PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass())) {
396                            baseClass = baseClass.getSuperclass();
397                    }
398                    Field f = baseClass.getDeclaredField("jdoStateManager");
399                    f.setAccessible(true);
400                    StateManager sm = (StateManager)f.get(o);
401                    if (sm != null) {
402                            setDetachedState((Detachable)o, null);
403                            sm.preSerialize(o);
404                    }
405            }
406            catch (Exception e) {
407                    throw new RuntimeException("Cannot access jdoDetachedState for detached object", e);
408            }
409        }
410        
411        private static Object[] getDetachedState(javax.jdo.spi.Detachable o) {
412            try {
413                    Class<?> baseClass = o.getClass();
414                    while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass()))
415                            baseClass = baseClass.getSuperclass();
416                    Field f = baseClass.getDeclaredField("jdoDetachedState");
417                    f.setAccessible(true);
418                    return (Object[])f.get(o);
419            }
420            catch (Exception e) {
421                    throw new RuntimeException("Cannot access jdoDetachedState for detached object", e);
422            }
423        }
424        
425        private static void setDetachedState(javax.jdo.spi.Detachable o, Object[] detachedState) {
426            try {
427                    Class<?> baseClass = o.getClass();
428                    while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass()))
429                            baseClass = baseClass.getSuperclass();
430                    Field f = baseClass.getDeclaredField("jdoDetachedState");
431                    f.setAccessible(true);
432                    f.set(o, detachedState);
433            }
434            catch (Exception e) {
435                    throw new RuntimeException("Cannot access jdoDetachedState for detached object", e);
436            }
437        }
438        
439        
440        static Map<String, Boolean> getLoadedState(Detachable pc, Class<?> clazz) {
441            return getLoadedState(getDetachedState(pc), clazz);     
442        }
443        
444        static Map<String, Boolean> getLoadedState(Object[] detachedState, Class<?> clazz) {
445            try {
446                    BitSet loaded = detachedState != null ? (BitSet)detachedState[2] : null;
447                    
448                    List<String> fieldNames = new ArrayList<String>();
449                    for (Class<?> c = clazz; c != null && PersistenceCapable.class.isAssignableFrom(c); c = c.getSuperclass()) { 
450                            Field pcFieldNames = c.getDeclaredField("jdoFieldNames");
451                            pcFieldNames.setAccessible(true);
452                            fieldNames.addAll(0, Arrays.asList((String[])pcFieldNames.get(null)));
453                    }
454                    
455                    Map<String, Boolean> loadedState = new HashMap<String, Boolean>();
456                    for (int i = 0; i < fieldNames.size(); i++)
457                            loadedState.put(fieldNames.get(i), (loaded != null && loaded.size() > i ? loaded.get(i) : true));
458                    return loadedState;
459            }
460            catch (Exception e) {
461                    throw new RuntimeException("Could not get loaded state for: " + detachedState);
462            }
463        }
464        
465        protected byte[] serializeDetachedState(Object[] detachedState) {
466            try {
467                    // Force version
468                    ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
469                    ObjectOutputStream oos = new ObjectOutputStream(baos);
470                    oos.writeObject(detachedState);
471                    return baos.toByteArray();
472            } catch (Exception e) {
473                    throw new RuntimeException("Could not serialize detached state for: " + detachedState);
474            }
475        }
476        
477        protected void deserializeDetachedState(Detachable pc, byte[] data) {
478            try {
479                    ByteArrayInputStream baos = new ByteArrayInputStream(data);
480                    ObjectInputStream oos = new ObjectInputStream(baos);
481                    Object[] state = (Object[])oos.readObject();
482                    setDetachedState(pc, state);
483            } catch (Exception e) {
484                    throw new RuntimeException("Could not deserialize detached state for: " + data);
485            }
486        }
487        
488        protected static Object getVersion(Object entity) {
489                    Class<?> entityClass = entity.getClass();
490                    
491            if (jpaEnabled && entityClass.isAnnotationPresent(entityAnnotation)) {
492                for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass())  {
493                    for (Method method : clazz.getDeclaredMethods()) {
494                                if (method.isAnnotationPresent(Version.class)) {
495                            return Reflections.invokeAndWrap(method, entity);
496                                }
497                            }                
498                }
499                
500                for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass())      {
501                    for (Field field : clazz.getDeclaredFields()) {
502                            if (field.isAnnotationPresent(Version.class)) {
503                                    if (!field.isAccessible())
504                                            field.setAccessible(true);
505                            return Reflections.getAndWrap(field, entity);
506                            }
507                   }
508                }
509                
510                return null;
511            }
512            else if (!jpaEnabled && entity instanceof PersistenceCapable) {
513                    if (entityClass.isAnnotationPresent(javax.jdo.annotations.Version.class)) {
514                            javax.jdo.annotations.Version version = entityClass.getAnnotation(javax.jdo.annotations.Version.class);
515                            for (Extension extension : version.extensions()) {
516                                    if (extension.vendorName().equals("datanucleus") && extension.key().equals("field-name")) {
517                                            String versionFieldName = extension.value();
518                                            
519                                            try {
520                                                    Method versionGetter = entityClass.getMethod("get" + versionFieldName.substring(0, 1).toUpperCase() + versionFieldName.substring(1));
521                                                    return Reflections.invokeAndWrap(versionGetter, entity);
522                                            }
523                                            catch (NoSuchMethodException e) {
524                                        for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass())      {
525                                            for (Field field : clazz.getDeclaredFields()) {
526                                                    if (field.getName().equals(versionFieldName)) {
527                                                            if (!field.isAccessible())
528                                                                    field.setAccessible(true);
529                                                    return Reflections.getAndWrap(field, entity);
530                                                    }
531                                           }
532                                        }
533                                            }                                       
534                                    } 
535                                    
536                            }
537                    }
538            }
539            
540            return null;
541        }
542    }