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.hibernate4;
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.io.Serializable;
031    import java.lang.reflect.InvocationTargetException;
032    import java.lang.reflect.ParameterizedType;
033    import java.lang.reflect.Type;
034    import java.util.Comparator;
035    import java.util.List;
036    import java.util.Map;
037    import java.util.Set;
038    import java.util.SortedMap;
039    import java.util.SortedSet;
040    import java.util.concurrent.ConcurrentHashMap;
041    
042    import javax.persistence.Embeddable;
043    import javax.persistence.Entity;
044    import javax.persistence.MappedSuperclass;
045    
046    import org.granite.collections.BasicMap;
047    import org.granite.config.GraniteConfig;
048    import org.granite.context.GraniteContext;
049    import org.granite.logging.Logger;
050    import org.granite.messaging.amf.io.convert.Converters;
051    import org.granite.messaging.amf.io.util.ClassGetter;
052    import org.granite.messaging.amf.io.util.MethodProperty;
053    import org.granite.messaging.amf.io.util.Property;
054    import org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer;
055    import org.granite.messaging.amf.io.util.externalizer.annotation.ExternalizedProperty;
056    import org.granite.messaging.persistence.AbstractExternalizablePersistentCollection;
057    import org.granite.messaging.persistence.ExternalizablePersistentBag;
058    import org.granite.messaging.persistence.ExternalizablePersistentList;
059    import org.granite.messaging.persistence.ExternalizablePersistentMap;
060    import org.granite.messaging.persistence.ExternalizablePersistentSet;
061    import org.granite.util.TypeUtil;
062    import org.granite.util.StringUtil;
063    import org.granite.util.XMap;
064    import org.hibernate.Hibernate;
065    import org.hibernate.annotations.Sort;
066    import org.hibernate.annotations.SortType;
067    import org.hibernate.collection.spi.PersistentCollection;
068    import org.hibernate.collection.internal.PersistentBag;
069    import org.hibernate.collection.internal.PersistentList;
070    import org.hibernate.collection.internal.PersistentMap;
071    import org.hibernate.collection.internal.PersistentSet;
072    import org.hibernate.collection.internal.PersistentSortedMap;
073    import org.hibernate.collection.internal.PersistentSortedSet;
074    import org.hibernate.proxy.HibernateProxy;
075    import org.hibernate.proxy.LazyInitializer;
076    
077    /**
078     * @author Franck WOLFF
079     */
080    public class HibernateExternalizer extends DefaultExternalizer {
081    
082            private static final Logger log = Logger.getLogger(HibernateExternalizer.class);
083            
084        private final ConcurrentHashMap<String, ProxyFactory> proxyFactories = new ConcurrentHashMap<String, ProxyFactory>();
085        
086        static enum SerializeMetadata {
087            YES,
088            NO,
089            LAZY
090        }
091        
092        private SerializeMetadata serializeMetadata = SerializeMetadata.NO;
093        
094    
095        /**
096         * Configure this externalizer with the values supplied in granite-config.xml.
097         * 
098         * <p>The only supported configuration option is 'hibernate-collection-metadata' with
099         * values in ['no' (default), 'yes' and 'lazy']. By default, collection metadata (key,
100         * role and snapshot) aren't serialized. If the value of the 'hibernate-collection-metadata'
101         * node is 'yes', metadata will be always serialized, while the 'lazy' value tells the
102         * externalizer to serialiaze metadata for uninitialized collections only.
103         * 
104         * <p>Configuration example (granite-config.xml):
105         * <pre>
106         * &lt;granite-config scan="true"&gt;
107         *   &lt;externalizers&gt;
108         *     &lt;configuration&gt;
109         *       &lt;hibernate-collection-metadata&gt;lazy&lt;/hibernate-collection-metadata&gt;
110         *     &lt;/configuration&gt;
111         *   &lt;/externalizers&gt;
112         * &lt;/granite-config&gt;
113         * </pre>
114         * 
115         * @param properties an XMap instance that contains the configuration node.
116         */
117        @Override
118            public void configure(XMap properties) {
119            super.configure(properties);
120            
121            if (properties != null) {
122                    String collectionmetadata = properties.get("hibernate-collection-metadata");
123                    if (collectionmetadata != null) {
124                            if ("no".equalsIgnoreCase(collectionmetadata))
125                                    serializeMetadata = SerializeMetadata.NO;
126                            else if ("yes".equalsIgnoreCase(collectionmetadata))
127                                    serializeMetadata = SerializeMetadata.YES;
128                            else if ("lazy".equalsIgnoreCase(collectionmetadata))
129                                    serializeMetadata = SerializeMetadata.LAZY;
130                            else
131                                    throw new RuntimeException("Illegal value for the 'hibernate-collection-metadata' option: " + collectionmetadata);
132                    }
133            }
134            }
135    
136            @Override
137        public Object newInstance(String type, ObjectInput in)
138            throws IOException, ClassNotFoundException, InstantiationException, InvocationTargetException, IllegalAccessException {
139    
140            // If type is not an entity (@Embeddable for example), we don't read initialized/detachedState
141            // and we fall back to DefaultExternalizer behavior.
142            Class<?> clazz = TypeUtil.forName(type);
143            if (!isRegularEntity(clazz))
144                return super.newInstance(type, in);
145            
146            // Read initialized flag.
147            boolean initialized = ((Boolean)in.readObject()).booleanValue();
148    
149            // Read detachedState.
150            String detachedState = (String)in.readObject();
151            
152            // New or initialized entity.
153            if (initialized)
154                return super.newInstance(type, in);
155    
156            // Actual proxy instantiation is deferred in order to keep consistent order in
157            // stored objects list (see AMF3Deserializer).
158            return newProxyInstantiator(proxyFactories, detachedState);
159        }
160            
161            protected Object newProxyInstantiator(ConcurrentHashMap<String, ProxyFactory> proxyFactories, String detachedState) {
162            return new HibernateProxyInstantiator(proxyFactories, detachedState);
163            }
164    
165        @Override
166        public void readExternal(Object o, ObjectInput in) throws IOException, ClassNotFoundException, IllegalAccessException {
167    
168            // Skip unserialized fields for proxies (only read id).
169            if (o instanceof HibernateProxyInstantiator) {
170                    log.debug("Reading Hibernate Proxy...");
171                ((HibernateProxyInstantiator)o).readId(in);
172            }
173            // @Embeddable or others...
174            else if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) {
175                    log.debug("Delegating non regular entity reading to DefaultExternalizer...");
176                super.readExternal(o, in);
177            }
178            // Regular @Entity or @MappedSuperclass
179            else {
180                GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
181    
182                Converters converters = config.getConverters();
183                ClassGetter classGetter = config.getClassGetter();
184                Class<?> oClass = classGetter.getClass(o);
185                ParameterizedType[] declaringTypes = TypeUtil.getDeclaringTypes(oClass);
186    
187                List<Property> fields = findOrderedFields(oClass, false);
188                log.debug("Reading entity %s with fields %s", oClass.getName(), fields);
189                for (Property field : fields) {
190                    Object value = in.readObject();
191                    if (!(field instanceof MethodProperty && field.isAnnotationPresent(ExternalizedProperty.class, true))) {
192                        
193                            if (value instanceof AbstractExternalizablePersistentCollection)
194                                    value = newHibernateCollection((AbstractExternalizablePersistentCollection)value, field);
195                        else if (!(value instanceof HibernateProxy)) {
196                            Type targetType = TypeUtil.resolveTypeVariable(field.getType(), field.getDeclaringClass(), declaringTypes);
197                                    value = converters.convert(value, targetType);
198                            }
199    
200                            field.setProperty(o, value, false);
201                    }
202                }
203            }
204        }
205        
206            protected PersistentCollection newHibernateCollection(AbstractExternalizablePersistentCollection value, Property field) {
207            final Type target = field.getType();
208            final boolean initialized = value.isInitialized();
209            final String metadata = value.getMetadata();
210                    final boolean dirty = value.isDirty();
211            final boolean sorted = (
212                    SortedSet.class.isAssignableFrom(TypeUtil.classOfType(target)) ||
213                    SortedMap.class.isAssignableFrom(TypeUtil.classOfType(target))
214            );
215            
216            Comparator<?> comparator = null;
217            if (sorted && field.isAnnotationPresent(Sort.class)) {
218                    Sort sort = field.getAnnotation(Sort.class);
219                    if (sort.type() == SortType.COMPARATOR) {
220                            try {
221                                    comparator = TypeUtil.newInstance(sort.comparator(), Comparator.class);
222                            } catch (Exception e) {
223                                    throw new RuntimeException("Could not create instance of Comparator: " + sort.comparator());
224                            }
225                    }
226            }
227            
228            PersistentCollection coll = null;
229                    if (value instanceof ExternalizablePersistentSet) {
230                    if (initialized) {
231                            Set<?> set = ((ExternalizablePersistentSet)value).getContentAsSet(target, comparator);
232                            coll = (sorted ? new PersistentSortedSet(null, (SortedSet<?>)set) : new PersistentSet(null, set));
233                }
234                    else
235                    coll = (sorted ? new PersistentSortedSet() : new PersistentSet());
236            }
237                    else if (value instanceof ExternalizablePersistentBag) {
238                    if (initialized) {
239                        List<?> bag = ((ExternalizablePersistentBag)value).getContentAsList(target);
240                    coll = new PersistentBag(null, bag);
241                    }
242                    else
243                        coll = new PersistentBag();
244                    }
245                    else if (value instanceof ExternalizablePersistentList) {
246                    if (initialized) {
247                        List<?> list = ((ExternalizablePersistentList)value).getContentAsList(target);
248                    coll = new PersistentList(null, list);
249                    }
250                    else
251                        coll = new PersistentList();
252                    }
253                    else if (value instanceof ExternalizablePersistentMap) {
254                    if (initialized) {
255                        Map<?, ?> map = ((ExternalizablePersistentMap)value).getContentAsMap(target, comparator);
256                        coll = (sorted ? new PersistentSortedMap(null, (SortedMap<?, ?>)map) : new PersistentMap(null, map));
257                    }
258                    else
259                        coll = (sorted ? new PersistentSortedMap() : new PersistentMap());
260                    }
261                    else
262                            throw new RuntimeException("Illegal externalizable persitent class: " + value);
263                    
264                    if (metadata != null && serializeMetadata != SerializeMetadata.NO && (serializeMetadata == SerializeMetadata.YES || !initialized)) {
265                    String[] toks = metadata.split(":", 3);
266                    if (toks.length != 3)
267                            throw new RuntimeException("Invalid collection metadata: " + metadata);
268                    Serializable key = deserializeSerializable(StringUtil.hexStringToBytes(toks[0]));
269                    Serializable snapshot = deserializeSerializable(StringUtil.hexStringToBytes(toks[1]));
270                    String role = toks[2];
271                coll.setSnapshot(key, role, snapshot);
272                    }
273                    
274                    if (initialized && dirty)
275                            coll.dirty();
276            
277            return coll;
278        }
279    
280        @Override
281        public void writeExternal(Object o, ObjectOutput out) throws IOException, IllegalAccessException {
282    
283            ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter();
284            Class<?> oClass = classGetter.getClass(o);
285    
286            String detachedState = null;
287            if (o instanceof HibernateProxy) {              
288                HibernateProxy proxy = (HibernateProxy)o;
289                detachedState = getProxyDetachedState(proxy);
290    
291                // Only write initialized flag & detachedState & entity id if proxy is uninitialized.
292                if (proxy.getHibernateLazyInitializer().isUninitialized()) {
293                    Serializable id = proxy.getHibernateLazyInitializer().getIdentifier();
294                    log.debug("Writing uninitialized HibernateProxy %s with id %s", detachedState, id);
295                    
296                    // Write initialized flag.
297                    out.writeObject(Boolean.FALSE);
298                    // Write detachedState.
299                    out.writeObject(detachedState);
300                    // Write entity id.
301                    out.writeObject(id);
302                    return;
303                }
304    
305                // Proxy is initialized, get the underlying persistent object.
306                    log.debug("Writing initialized HibernateProxy...");
307                o = proxy.getHibernateLazyInitializer().getImplementation();
308            }
309    
310            if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) { // @Embeddable or others...
311                    log.debug("Delegating non regular entity writing to DefaultExternalizer...");
312                super.writeExternal(o, out);
313            }
314            else {
315                    if (isRegularEntity(o.getClass())) {
316                        // Write initialized flag.
317                        out.writeObject(Boolean.TRUE);
318                        // Write detachedState.
319                        out.writeObject(null);
320                    }
321                    
322                // Externalize entity fields.
323                List<Property> fields = findOrderedFields(oClass, false);
324                log.debug("Writing entity %s with fields %s", o.getClass().getName(), fields);
325                for (Property field : fields) {
326                    Object value = field.getProperty(o);
327                    
328                    // Persistent collections.
329                    if (value instanceof PersistentCollection)
330                            value = newExternalizableCollection((PersistentCollection)value);
331                    // Transient maps.
332                    else if (value instanceof Map<?, ?>)
333                            value = BasicMap.newInstance((Map<?, ?>)value);
334    
335                    if (isValueIgnored(value))
336                            out.writeObject(null);
337                    else
338                            out.writeObject(value);
339                }
340            }
341        }
342        
343        protected AbstractExternalizablePersistentCollection newExternalizableCollection(PersistentCollection value) {
344            final boolean initialized = Hibernate.isInitialized(value);
345            final boolean dirty = value.isDirty();
346            
347            AbstractExternalizablePersistentCollection coll = null;
348            
349            if (value instanceof PersistentSet)
350                coll = new ExternalizablePersistentSet(initialized ? (Set<?>)value : null, initialized, dirty);
351            else if (value instanceof PersistentList)
352                coll = new ExternalizablePersistentList(initialized ? (List<?>)value : null, initialized, dirty);
353            else if (value instanceof PersistentBag)
354                coll = new ExternalizablePersistentBag(initialized ? (List<?>)value : null, initialized, dirty);
355            else if (value instanceof PersistentMap)
356                coll = new ExternalizablePersistentMap(initialized ? (Map<?, ?>)value : null, initialized, dirty);
357            else
358                throw new UnsupportedOperationException("Unsupported Hibernate collection type: " + value);
359    
360            if (serializeMetadata != SerializeMetadata.NO && (serializeMetadata == SerializeMetadata.YES || !initialized) && value.getRole() != null) {
361                    char[] hexKey = StringUtil.bytesToHexChars(serializeSerializable(value.getKey()));
362                    char[] hexSnapshot = StringUtil.bytesToHexChars(serializeSerializable(value.getStoredSnapshot()));
363                    String metadata = new StringBuilder(hexKey.length + 1 + hexSnapshot.length + 1 + value.getRole().length())
364                                    .append(hexKey).append(':')
365                            .append(hexSnapshot).append(':')
366                            .append(value.getRole())
367                            .toString();
368                    coll.setMetadata(metadata);
369            }
370            
371            return coll;
372        }
373    
374        @Override
375        public int accept(Class<?> clazz) {
376            return (
377                clazz.isAnnotationPresent(Entity.class) ||
378                clazz.isAnnotationPresent(MappedSuperclass.class) ||
379                clazz.isAnnotationPresent(Embeddable.class)
380            ) ? 1 : -1;
381        }
382    
383        protected String getProxyDetachedState(HibernateProxy proxy) {
384            LazyInitializer initializer = proxy.getHibernateLazyInitializer();
385    
386            StringBuilder sb = new StringBuilder();
387    
388            sb.append(initializer.getClass().getName())
389              .append(':');
390            if (initializer.getPersistentClass() != null)
391                sb.append(initializer.getPersistentClass().getName());
392            sb.append(':');
393            if (initializer.getEntityName() != null)
394                sb.append(initializer.getEntityName());
395    
396            return sb.toString();
397        }
398    
399        protected boolean isRegularEntity(Class<?> clazz) {
400            return clazz.isAnnotationPresent(Entity.class) || clazz.isAnnotationPresent(MappedSuperclass.class);
401        }
402    
403        protected boolean isEmbeddable(Class<?> clazz) {
404            return clazz.isAnnotationPresent(Embeddable.class);
405        }    
406        
407        protected byte[] serializeSerializable(Serializable o) {
408            if (o == null)
409                    return BYTES_0;
410            try {
411                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
412                    ObjectOutputStream oos = new ObjectOutputStream(baos);
413                    oos.writeObject(o);
414                    return baos.toByteArray();
415            } catch (Exception e) {
416                    throw new RuntimeException("Could not serialize: " + o);
417            }
418        }
419        
420        protected Serializable deserializeSerializable(byte[] data) {
421            if (data.length == 0)
422                    return null;
423            try {
424                    ByteArrayInputStream baos = new ByteArrayInputStream(data);
425                    ObjectInputStream oos = new ObjectInputStream(baos);
426                    return (Serializable)oos.readObject();
427            } catch (Exception e) {
428                    throw new RuntimeException("Could not deserialize: " + data);
429            }
430        }
431    }