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