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())) {
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())) { // @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                    // Pseudo-proxy created for uninitialized entities (see below).
217                    if (detachedState != null && detachedState[0] == NULL_ID) {
218                    // Write initialized flag.
219                    out.writeObject(Boolean.FALSE);
220                    // Write detached state.
221                            out.writeObject(null);
222                            // Write id.
223                            out.writeObject(null);
224                            return;
225                    }
226    
227                    // Write initialized flag.
228                    out.writeObject(Boolean.TRUE);
229                    
230                    if (detachedState != null) {
231                    // Write detached state as a String, in the form of an hex representation
232                    // of the serialized detached state.
233                    org.granite.util.Entity entity = new org.granite.util.Entity(pco);
234                    Object version = entity.getVersion();
235                    if (version != null)
236                            detachedState[1] = version;
237                            byte[] binDetachedState = serializeDetachedState(detachedState);
238                            char[] hexDetachedState = StringUtil.bytesToHexChars(binDetachedState);
239                        out.writeObject(new String(hexDetachedState));
240                    }
241                    else
242                            out.writeObject(null);
243    
244                // Externalize entity fields.
245                List<Property> fields = findOrderedFields(oClass);
246                    Map<String, Boolean> loadedState = getLoadedState(detachedState, oClass);
247                log.debug("Writing entity %s with fields %s", o.getClass().getName(), fields);
248                for (Property field : fields) {
249                    if (field.getName().equals("jdoDetachedState"))
250                            continue;
251                    
252                    Object value = field.getProperty(o);
253                    if (isValueIgnored(value)) {
254                            out.writeObject(null);
255                            continue;
256                    }
257                    
258                    // Uninitialized associations.
259                    if (loadedState.containsKey(field.getName()) && !loadedState.get(field.getName())) {
260                            Class<?> fieldClass = ClassUtil.classOfType(field.getType());
261                                    
262                            // Create a "pseudo-proxy" for uninitialized entities: detached state is set to "0" (uninitialized flag).
263                            if (Detachable.class.isAssignableFrom(fieldClass)) {
264                                    try {
265                                            value = fieldClass.newInstance();
266                                    } catch (Exception e) {
267                                            throw new RuntimeException("Could not create DataNucleus pseudo-proxy for: " + field, e);
268                                    }
269                                    setDetachedState((Detachable)value, new Object[] { NULL_ID, null, null, null });
270                            }
271                            // Create pseudo-proxy for collections (set or list).
272                            else if (Collection.class.isAssignableFrom(fieldClass)) {
273                                    if (Set.class.isAssignableFrom(fieldClass))
274                                            value = new ExternalizablePersistentSet((Set<?>)null, false, false);
275                                    else
276                                            value = new ExternalizablePersistentList((List<?>)null, false, false);
277                            }
278                            // Create pseudo-proxy for maps.
279                            else if (Map.class.isAssignableFrom(fieldClass)) {
280                                    value = new ExternalizablePersistentMap((Map<?, ?>)null, false, false);
281                            }
282                    }
283                    
284                    // Initialized collections.
285                    else if (value instanceof Set<?>) {
286                            value = new ExternalizablePersistentSet(((Set<?>)value).toArray(), true, false);
287                    }
288                    else if (value instanceof List<?>) {
289                            value = new ExternalizablePersistentList(((List<?>)value).toArray(), true, false);
290                    }
291                    else if (value instanceof Map<?, ?>) {
292                            value = new ExternalizablePersistentMap((Map<?, ?>)null, true, false);
293                            ((ExternalizablePersistentMap)value).setContentFromMap((Map<?, ?>)value);
294                    }
295                    out.writeObject(value);
296                }
297            }
298        }
299    
300        @Override
301        public int accept(Class<?> clazz) {
302            return (
303                clazz.isAnnotationPresent(Entity.class) ||
304                clazz.isAnnotationPresent(MappedSuperclass.class) ||
305                clazz.isAnnotationPresent(Embeddable.class) ||
306                clazz.isAnnotationPresent(javax.jdo.annotations.PersistenceCapable.class)
307            ) ? 1 : -1;
308        }
309    
310        protected boolean isRegularEntity(Class<?> clazz) {
311            return ((PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz)) 
312                    || clazz.isAnnotationPresent(Entity.class) || clazz.isAnnotationPresent(MappedSuperclass.class))
313                    && !(clazz.isAnnotationPresent(Embeddable.class));
314        }
315    
316        @Override
317        public List<Property> findOrderedFields(final Class<?> clazz, boolean returnSettersWhenAvailable) {
318            List<Property> orderedFields = super.findOrderedFields(clazz, returnSettersWhenAvailable);
319            if (clazz.isAnnotationPresent(Embeddable.class)) {
320                    Iterator<Property> ifield = orderedFields.iterator();
321                    while (ifield.hasNext()) {
322                            Property field = ifield.next();
323                            if (field.getName().equals("jdoDetachedState"))
324                                    ifield.remove();
325                    }
326            }
327            return orderedFields;
328        }
329        
330            
331        private static void preSerialize(PersistenceCapable o) {
332            try {
333                    Class<?> baseClass = o.getClass();
334                    while (baseClass.getSuperclass() != Object.class &&
335                               baseClass.getSuperclass() != null &&
336                               PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass())) {
337                            baseClass = baseClass.getSuperclass();
338                    }
339                    Field f = baseClass.getDeclaredField("jdoStateManager");
340                    f.setAccessible(true);
341                    StateManager sm = (StateManager)f.get(o);
342                    if (sm != null) {
343                            setDetachedState((Detachable)o, null);
344                            sm.preSerialize(o);
345                    }
346            }
347            catch (Exception e) {
348                    throw new RuntimeException("Cannot access jdoDetachedState for detached object", e);
349            }
350        }
351        
352        private static Object[] getDetachedState(javax.jdo.spi.Detachable o) {
353            try {
354                    Class<?> baseClass = o.getClass();
355                    while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass()))
356                            baseClass = baseClass.getSuperclass();
357                    Field f = baseClass.getDeclaredField("jdoDetachedState");
358                    f.setAccessible(true);
359                    return (Object[])f.get(o);
360            }
361            catch (Exception e) {
362                    throw new RuntimeException("Cannot access jdoDetachedState for detached object", e);
363            }
364        }
365        
366        private static void setDetachedState(javax.jdo.spi.Detachable o, Object[] detachedState) {
367            try {
368                    Class<?> baseClass = o.getClass();
369                    while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass()))
370                            baseClass = baseClass.getSuperclass();
371                    Field f = baseClass.getDeclaredField("jdoDetachedState");
372                    f.setAccessible(true);
373                    f.set(o, detachedState);
374            }
375            catch (Exception e) {
376                    throw new RuntimeException("Cannot access jdoDetachedState for detached object", e);
377            }
378        }
379        
380        
381        static Map<String, Boolean> getLoadedState(Detachable pc, Class<?> clazz) {
382            return getLoadedState(getDetachedState(pc), clazz);     
383        }
384        
385        static Map<String, Boolean> getLoadedState(Object[] detachedState, Class<?> clazz) {
386            try {
387                    BitSet loaded = detachedState != null ? (BitSet)detachedState[2] : null;
388                    
389                    List<String> fieldNames = new ArrayList<String>();
390                    for (Class<?> c = clazz; c != null && PersistenceCapable.class.isAssignableFrom(c); c = c.getSuperclass()) { 
391                            Field pcFieldNames = c.getDeclaredField("jdoFieldNames");
392                            pcFieldNames.setAccessible(true);
393                            fieldNames.addAll(0, Arrays.asList((String[])pcFieldNames.get(null)));
394                    }
395                    
396                    Map<String, Boolean> loadedState = new HashMap<String, Boolean>();
397                    for (int i = 0; i < fieldNames.size(); i++)
398                            loadedState.put(fieldNames.get(i), (loaded != null && loaded.size() > i ? loaded.get(i) : true));
399                    return loadedState;
400            }
401            catch (Exception e) {
402                    throw new RuntimeException("Could not get loaded state for: " + detachedState);
403            }
404        }
405        
406        protected byte[] serializeDetachedState(Object[] detachedState) {
407            try {
408                    // Force version
409                    ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
410                    ObjectOutputStream oos = new ObjectOutputStream(baos);
411                    oos.writeObject(detachedState);
412                    return baos.toByteArray();
413            } catch (Exception e) {
414                    throw new RuntimeException("Could not serialize detached state for: " + detachedState);
415            }
416        }
417        
418        protected void deserializeDetachedState(Detachable pc, byte[] data) {
419            try {
420                    ByteArrayInputStream baos = new ByteArrayInputStream(data);
421                    ObjectInputStream oos = new ObjectInputStream(baos);
422                    Object[] state = (Object[])oos.readObject();
423                    setDetachedState(pc, state);
424            } catch (Exception e) {
425                    throw new RuntimeException("Could not deserialize detached state for: " + data);
426            }
427        }
428    }