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