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
021package org.granite.datanucleus;
022
023import java.io.ByteArrayInputStream;
024import java.io.ByteArrayOutputStream;
025import java.io.IOException;
026import java.io.ObjectInput;
027import java.io.ObjectInputStream;
028import java.io.ObjectOutput;
029import java.io.ObjectOutputStream;
030import java.lang.annotation.Annotation;
031import java.lang.reflect.Field;
032import java.lang.reflect.InvocationTargetException;
033import java.lang.reflect.Method;
034import java.lang.reflect.ParameterizedType;
035import java.lang.reflect.Type;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.BitSet;
039import java.util.Collection;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.Iterator;
043import java.util.List;
044import java.util.Map;
045import java.util.Set;
046import java.util.SortedMap;
047import java.util.SortedSet;
048import java.util.TreeMap;
049import java.util.TreeSet;
050
051import javax.jdo.annotations.EmbeddedOnly;
052import javax.jdo.annotations.Extension;
053import javax.jdo.spi.Detachable;
054import javax.jdo.spi.PersistenceCapable;
055import javax.jdo.spi.StateManager;
056import javax.persistence.Version;
057
058import org.granite.config.GraniteConfig;
059import org.granite.context.GraniteContext;
060import org.granite.logging.Logger;
061import org.granite.messaging.amf.io.convert.Converters;
062import org.granite.messaging.amf.io.util.ClassGetter;
063import org.granite.messaging.amf.io.util.MethodProperty;
064import org.granite.messaging.amf.io.util.Property;
065import org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer;
066import org.granite.messaging.amf.io.util.externalizer.annotation.ExternalizedProperty;
067import org.granite.messaging.persistence.AbstractExternalizablePersistentCollection;
068import org.granite.messaging.persistence.ExternalizablePersistentList;
069import org.granite.messaging.persistence.ExternalizablePersistentMap;
070import org.granite.messaging.persistence.ExternalizablePersistentSet;
071import org.granite.util.TypeUtil;
072import org.granite.util.Reflections;
073import org.granite.util.StringUtil;
074
075
076/**
077 * @author Stephen MORE
078 * @author William DRAI
079 */
080@SuppressWarnings("unchecked")
081public 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}