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