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