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.openjpa;
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.ParameterizedType;
033    import java.lang.reflect.Type;
034    import java.util.ArrayList;
035    import java.util.Arrays;
036    import java.util.BitSet;
037    import java.util.Collection;
038    import java.util.HashMap;
039    import java.util.List;
040    import java.util.Map;
041    import java.util.Set;
042    
043    import javax.persistence.Embeddable;
044    import javax.persistence.Entity;
045    import javax.persistence.IdClass;
046    import javax.persistence.MappedSuperclass;
047    
048    import org.apache.openjpa.enhance.PersistenceCapable;
049    import org.apache.openjpa.kernel.OpenJPAStateManager;
050    import org.apache.openjpa.util.ProxyCollection;
051    import org.apache.openjpa.util.ProxyMap;
052    import org.granite.collections.BasicMap;
053    import org.granite.config.GraniteConfig;
054    import org.granite.context.GraniteContext;
055    import org.granite.logging.Logger;
056    import org.granite.messaging.amf.io.convert.Converters;
057    import org.granite.messaging.amf.io.util.ClassGetter;
058    import org.granite.messaging.amf.io.util.MethodProperty;
059    import org.granite.messaging.amf.io.util.Property;
060    import org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer;
061    import org.granite.messaging.amf.io.util.externalizer.annotation.ExternalizedProperty;
062    import org.granite.messaging.persistence.AbstractExternalizablePersistentCollection;
063    import org.granite.messaging.persistence.ExternalizablePersistentList;
064    import org.granite.messaging.persistence.ExternalizablePersistentMap;
065    import org.granite.messaging.persistence.ExternalizablePersistentSet;
066    import org.granite.util.TypeUtil;
067    import org.granite.util.StringUtil;
068    
069    /**
070     * @author Franck WOLFF
071     */
072    public class OpenJpaExternalizer extends DefaultExternalizer {
073    
074            private static final Logger log = Logger.getLogger(OpenJpaExternalizer.class);
075    
076        @Override
077        public Object newInstance(String type, ObjectInput in)
078            throws IOException, ClassNotFoundException, InstantiationException, InvocationTargetException, IllegalAccessException {
079    
080            // If type is not an entity (@Embeddable for example), we don't read initialized/detachedState
081            // and we fall back to DefaultExternalizer behavior.
082            Class<?> clazz = TypeUtil.forName(type);
083            if (!isRegularEntity(clazz))
084                return super.newInstance(type, in);
085            
086            // Read initialized flag.
087            boolean initialized = ((Boolean)in.readObject()).booleanValue();
088    
089            // Read detached state...
090            String detachedState = (String)in.readObject();
091            
092            // Pseudo-proxy (uninitialized entity).
093            if (!initialized) {
094                    Object id = in.readObject();
095                    if (id != null && (!clazz.isAnnotationPresent(IdClass.class) || !clazz.getAnnotation(IdClass.class).value().equals(id.getClass())))
096                            throw new RuntimeException("Id for OpenJPA pseudo-proxy should be null or IdClass (" + type + ")");
097                    return null;
098            }
099            
100            // New entity.
101            if (detachedState == null)
102                    return super.newInstance(type, in);
103    
104            // Existing entity.
105                    Object entity = clazz.newInstance();
106                    if (detachedState.length() > 0) {
107                    byte[] data = StringUtil.hexStringToBytes(detachedState);
108                            ((PersistenceCapable)entity).pcSetDetachedState(deserializeDetachedState(data));
109                    }
110                    return entity;
111        }
112    
113        @Override
114        public void readExternal(Object o, ObjectInput in) throws IOException, ClassNotFoundException, IllegalAccessException {
115    
116            if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) {
117                    log.debug("Delegating non regular entity reading to DefaultExternalizer...");
118                super.readExternal(o, in);
119            }
120            // Regular @Entity or @MappedSuperclass
121            else {
122                GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
123    
124                Converters converters = config.getConverters();
125                ClassGetter classGetter = config.getClassGetter();
126                Class<?> oClass = classGetter.getClass(o);
127                ParameterizedType[] declaringTypes = TypeUtil.getDeclaringTypes(oClass);
128    
129                List<Property> fields = findOrderedFields(oClass);
130                log.debug("Reading entity %s with fields %s", oClass.getName(), fields);
131                for (Property field : fields) {
132                    Object value = in.readObject();
133                    
134                    if (!(field instanceof MethodProperty && field.isAnnotationPresent(ExternalizedProperty.class, true))) {
135                            
136                            // (Un)Initialized collections/maps.
137                            if (value instanceof AbstractExternalizablePersistentCollection) {
138                                    // Uninitialized.
139                                    if (!((AbstractExternalizablePersistentCollection)value).isInitialized())
140                                            value = null;
141                                    // Initialized.
142                                    else {
143                                            if (value instanceof ExternalizablePersistentSet)
144                                                    value = ((ExternalizablePersistentSet)value).getContentAsSet(field.getType());
145                                            else if (value instanceof ExternalizablePersistentMap)
146                                                    value = ((ExternalizablePersistentMap)value).getContentAsMap(field.getType());
147                                            else
148                                                    value = ((ExternalizablePersistentList)value).getContentAsList(field.getType());
149                                    }
150                            }
151                            // Others...
152                        else {
153                            Type targetType = TypeUtil.resolveTypeVariable(field.getType(), field.getDeclaringClass(), declaringTypes);
154                                    value = converters.convert(value, targetType);
155                        }
156                        
157                            field.setProperty(o, value, false);
158                    }
159                }
160            }
161        }
162    
163        @Override
164        public void writeExternal(Object o, ObjectOutput out) throws IOException, IllegalAccessException {
165    
166            ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter();
167            Class<?> oClass = classGetter.getClass(o);
168    
169            if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) { // @Embeddable or others...
170                    log.debug("Delegating non regular entity writing to DefaultExternalizer...");
171                super.writeExternal(o, out);
172            }
173            else {
174                    PersistenceCapable pco = (PersistenceCapable)o;
175                    
176                    if (isRegularEntity(o.getClass())) {
177                            // Pseudo-proxy created for uninitialized entities (see below).
178                            if (Boolean.FALSE.equals(pco.pcGetDetachedState())) {
179                            // Write uninitialized flag.
180                            out.writeObject(Boolean.FALSE);
181                            // Write detached state.
182                                    out.writeObject(null);
183                                    // Write id.
184                                    out.writeObject(null);
185                                    return;
186                            }
187            
188                            // Write initialized flag.
189                            out.writeObject(Boolean.TRUE);
190            
191                            // Write detached state as a String, in the form of an hex representation
192                            // of the serialized detached state.
193                            byte[] detachedState = serializeDetachedState(pco);
194                            char[] hexDetachedState = StringUtil.bytesToHexChars(detachedState);
195                        out.writeObject(new String(hexDetachedState));
196                    }
197    
198                // Externalize entity fields.
199                List<Property> fields = findOrderedFields(oClass);
200                    Map<String, Boolean> loadedState = getLoadedState(pco, oClass);
201                log.debug("Writing entity %s with fields %s", o.getClass().getName(), fields);
202                for (Property field : fields) {
203                    Object value = field.getProperty(o);
204                    
205                    // Uninitialized associations.
206                    if (value == null && loadedState.containsKey(field.getName()) && !loadedState.get(field.getName())) {
207                            Class<?> fieldClass = TypeUtil.classOfType(field.getType());
208                                    
209                            // Create a "pseudo-proxy" for uninitialized entities: detached state is set to
210                            // Boolean.FALSE (uninitialized flag).
211                            if (PersistenceCapable.class.isAssignableFrom(fieldClass)) {
212                                    try {
213                                            value = fieldClass.newInstance();
214                                    } catch (Exception e) {
215                                            throw new RuntimeException("Could not create OpenJPA pseudo-proxy for: " + field, e);
216                                    }
217                                    ((PersistenceCapable)value).pcSetDetachedState(Boolean.FALSE);
218                            }
219                            // Create pseudo-proxy for collections (set or list).
220                            else if (Collection.class.isAssignableFrom(fieldClass)) {
221                                    if (Set.class.isAssignableFrom(fieldClass))
222                                            value = new ExternalizablePersistentSet((Object[])null, false, false);
223                                    else
224                                            value = new ExternalizablePersistentList((Object[])null, false, false);
225                            }
226                            // Create pseudo-proxy for maps.
227                            else if (Map.class.isAssignableFrom(fieldClass)) {
228                                    value = new ExternalizablePersistentMap((Object[])null, false, false);
229                            }
230                    }
231                    
232                    // Initialized collections.
233                    else if (value instanceof ProxyCollection) {
234                            if (value instanceof Set<?>)
235                                    value = new ExternalizablePersistentSet(((ProxyCollection)value).toArray(), true, false);
236                            else
237                                    value = new ExternalizablePersistentList(((ProxyCollection)value).toArray(), true, false);
238                    }
239                    
240                    // Initialized maps.
241                    else if (value instanceof ProxyMap) {
242                            value = new ExternalizablePersistentMap((Object[])null, true, false);
243                            ((ExternalizablePersistentMap)value).setContentFromMap((ProxyMap)value);
244                    }
245                    
246                    // Transient maps.
247                    else if (value instanceof Map<?, ?>)
248                            value = BasicMap.newInstance((Map<?, ?>)value);
249                    
250                    out.writeObject(value);
251                }
252            }
253        }
254    
255        @Override
256        public int accept(Class<?> clazz) {
257            return (
258                clazz.isAnnotationPresent(Entity.class) ||
259                clazz.isAnnotationPresent(MappedSuperclass.class) ||
260                clazz.isAnnotationPresent(Embeddable.class)
261            ) ? 1 : -1;
262        }
263    
264        protected boolean isRegularEntity(Class<?> clazz) {
265            return PersistenceCapable.class.isAssignableFrom(clazz) && (
266                    clazz.isAnnotationPresent(Entity.class) || clazz.isAnnotationPresent(MappedSuperclass.class)
267            );
268        }
269    
270        protected boolean isEmbeddable(Class<?> clazz) {
271            return PersistenceCapable.class.isAssignableFrom(clazz) && clazz.isAnnotationPresent(Embeddable.class);
272        }
273        
274        // Very hacky!
275        static Map<String, Boolean> getLoadedState(PersistenceCapable pc, Class<?> clazz) {
276            try {
277                    BitSet loaded = null;
278                    if (pc.pcGetStateManager() instanceof OpenJPAStateManager) {
279                            OpenJPAStateManager sm = (OpenJPAStateManager)pc.pcGetStateManager();
280                            loaded = sm.getLoaded();
281                    }
282                    // State manager may be null for some entities...
283                    if (loaded == null) {
284                            Object ds = pc.pcGetDetachedState();
285                            if (ds != null && ds.getClass().isArray()) {
286                                    Object[] dsa = (Object[])ds;
287                                    if (dsa.length > 1 && dsa[1] instanceof BitSet)
288                                            loaded = (BitSet)dsa[1];
289                            }
290                    }
291                    
292                    List<String> fieldNames = new ArrayList<String>();
293                    for (Class<?> c = clazz; c != null && PersistenceCapable.class.isAssignableFrom(c); c = c.getSuperclass()) { 
294                            Field pcFieldNames = c.getDeclaredField("pcFieldNames");
295                            pcFieldNames.setAccessible(true);
296                            fieldNames.addAll(0, Arrays.asList((String[])pcFieldNames.get(null)));
297                    }
298                    
299                    Map<String, Boolean> loadedState = new HashMap<String, Boolean>();
300                    for (int i = 0; i < fieldNames.size(); i++)
301                            loadedState.put(fieldNames.get(i), (loaded != null && loaded.size() > i ? loaded.get(i) : true));
302                    return loadedState;
303            }
304            catch (Exception e) {
305                    throw new RuntimeException("Could not get loaded state for: " + pc);
306            }
307        }
308        
309        protected byte[] serializeDetachedState(PersistenceCapable pc) {
310            try {
311                    ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
312                    ObjectOutputStream oos = new ObjectOutputStream(baos);
313                    oos.writeObject(pc.pcGetDetachedState());
314                    return baos.toByteArray();
315            } catch (Exception e) {
316                    throw new RuntimeException("Could not serialize detached state for: " + pc);
317            }
318        }
319        
320        protected Object deserializeDetachedState(byte[] data) {
321            try {
322                    ByteArrayInputStream baos = new ByteArrayInputStream(data);
323                    ObjectInputStream oos = new ObjectInputStream(baos);
324                    return oos.readObject();
325            } catch (Exception e) {
326                    throw new RuntimeException("Could not deserialize detached state for: " + data);
327            }
328        }
329    }