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.hibernate4;
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.internal.PersistentBag;
069import org.hibernate.collection.internal.PersistentList;
070import org.hibernate.collection.internal.PersistentMap;
071import org.hibernate.collection.internal.PersistentSet;
072import org.hibernate.collection.internal.PersistentSortedMap;
073import org.hibernate.collection.internal.PersistentSortedSet;
074import org.hibernate.collection.spi.PersistentCollection;
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        if (o instanceof HibernateProxy) {              
289            HibernateProxy proxy = (HibernateProxy)o;
290            detachedState = getProxyDetachedState(proxy);
291
292            // Only write initialized flag & detachedState & entity id if proxy is uninitialized.
293            if (proxy.getHibernateLazyInitializer().isUninitialized()) {
294                Serializable id = proxy.getHibernateLazyInitializer().getIdentifier();
295                log.debug("Writing uninitialized HibernateProxy %s with id %s", detachedState, id);
296                
297                // Write initialized flag.
298                out.writeObject(Boolean.FALSE);
299                // Write detachedState.
300                out.writeObject(detachedState);
301                // Write entity id.
302                out.writeObject(id);
303                return;
304            }
305
306            // Proxy is initialized, get the underlying persistent object.
307                log.debug("Writing initialized HibernateProxy...");
308            o = proxy.getHibernateLazyInitializer().getImplementation();
309        }
310
311        if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) { // @Embeddable or others...
312                log.debug("Delegating non regular entity writing to DefaultExternalizer...");
313            super.writeExternal(o, out);
314        }
315        else {
316                if (isRegularEntity(o.getClass())) {
317                    // Write initialized flag.
318                    out.writeObject(Boolean.TRUE);
319                    // Write detachedState.
320                    out.writeObject(null);
321                }
322                
323            // Externalize entity fields.
324            List<Property> fields = findOrderedFields(oClass, false);
325            log.debug("Writing entity %s with fields %s", o.getClass().getName(), fields);
326            for (Property field : fields) {
327                Object value = field.getProperty(o);
328                
329                // Persistent collections.
330                if (value instanceof PersistentCollection)
331                        value = newExternalizableCollection((PersistentCollection)value);
332                // Transient maps.
333                else if (value instanceof Map<?, ?>)
334                        value = BasicMap.newInstance((Map<?, ?>)value);
335
336                if (isValueIgnored(value))
337                        out.writeObject(null);
338                else
339                        out.writeObject(value);
340            }
341        }
342    }
343    
344    protected AbstractExternalizablePersistentCollection newExternalizableCollection(PersistentCollection value) {
345        final boolean initialized = Hibernate.isInitialized(value);
346        final boolean dirty = value.isDirty();
347        
348        AbstractExternalizablePersistentCollection coll = null;
349        
350        if (value instanceof PersistentSet)
351            coll = new ExternalizablePersistentSet(initialized ? (Set<?>)value : null, initialized, dirty);
352        else if (value instanceof PersistentList)
353            coll = new ExternalizablePersistentList(initialized ? (List<?>)value : null, initialized, dirty);
354        else if (value instanceof PersistentBag)
355            coll = new ExternalizablePersistentBag(initialized ? (List<?>)value : null, initialized, dirty);
356        else if (value instanceof PersistentMap)
357            coll = new ExternalizablePersistentMap(initialized ? (Map<?, ?>)value : null, initialized, dirty);
358        else
359            throw new UnsupportedOperationException("Unsupported Hibernate collection type: " + value);
360
361        if (serializeMetadata != SerializeMetadata.NO && (serializeMetadata == SerializeMetadata.YES || !initialized) && value.getRole() != null) {
362                char[] hexKey = StringUtil.bytesToHexChars(serializeSerializable(value.getKey()));
363                char[] hexSnapshot = StringUtil.bytesToHexChars(serializeSerializable(value.getStoredSnapshot()));
364                String metadata = new StringBuilder(hexKey.length + 1 + hexSnapshot.length + 1 + value.getRole().length())
365                                .append(hexKey).append(':')
366                        .append(hexSnapshot).append(':')
367                        .append(value.getRole())
368                        .toString();
369                coll.setMetadata(metadata);
370        }
371        
372        return coll;
373    }
374
375    @Override
376    public int accept(Class<?> clazz) {
377        return (
378            clazz.isAnnotationPresent(Entity.class) ||
379            clazz.isAnnotationPresent(MappedSuperclass.class) ||
380            clazz.isAnnotationPresent(Embeddable.class)
381        ) ? 1 : -1;
382    }
383
384    protected String getProxyDetachedState(HibernateProxy proxy) {
385        LazyInitializer initializer = proxy.getHibernateLazyInitializer();
386
387        StringBuilder sb = new StringBuilder();
388
389        sb.append(initializer.getClass().getName())
390          .append(':');
391        if (initializer.getPersistentClass() != null)
392            sb.append(initializer.getPersistentClass().getName());
393        sb.append(':');
394        if (initializer.getEntityName() != null)
395            sb.append(initializer.getEntityName());
396
397        return sb.toString();
398    }
399
400    protected boolean isRegularEntity(Class<?> clazz) {
401        return clazz.isAnnotationPresent(Entity.class) || clazz.isAnnotationPresent(MappedSuperclass.class);
402    }
403
404    protected boolean isEmbeddable(Class<?> clazz) {
405        return clazz.isAnnotationPresent(Embeddable.class);
406    }    
407    
408    protected byte[] serializeSerializable(Serializable o) {
409        if (o == null)
410                return BYTES_0;
411        try {
412                ByteArrayOutputStream baos = new ByteArrayOutputStream();
413                ObjectOutputStream oos = new ObjectOutputStream(baos);
414                oos.writeObject(o);
415                return baos.toByteArray();
416        } catch (Exception e) {
417                throw new RuntimeException("Could not serialize: " + o);
418        }
419    }
420    
421    protected Serializable deserializeSerializable(byte[] data) {
422        if (data.length == 0)
423                return null;
424        try {
425                ByteArrayInputStream baos = new ByteArrayInputStream(data);
426                ObjectInputStream oos = new ObjectInputStream(baos);
427                return (Serializable)oos.readObject();
428        } catch (Exception e) {
429                throw new RuntimeException("Could not deserialize: " + data);
430        }
431    }
432}