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