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