001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2012 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.client.tide.data.impl;
022    
023    import java.lang.reflect.Array;
024    import java.util.ArrayList;
025    import java.util.Collection;
026    import java.util.Collections;
027    import java.util.HashSet;
028    import java.util.IdentityHashMap;
029    import java.util.Iterator;
030    import java.util.List;
031    import java.util.Map;
032    import java.util.Map.Entry;
033    import java.util.Set;
034    
035    import org.granite.client.persistence.LazyableCollection;
036    import org.granite.client.tide.Context;
037    import org.granite.client.tide.SyncMode;
038    import org.granite.client.tide.collections.ManagedPersistentAssociation;
039    import org.granite.client.tide.collections.ManagedPersistentCollection;
040    import org.granite.client.tide.collections.ManagedPersistentMap;
041    import org.granite.client.tide.data.Conflict;
042    import org.granite.client.tide.data.DataConflictListener;
043    import org.granite.client.tide.data.DataMerger;
044    import org.granite.client.tide.data.EntityManager;
045    import org.granite.client.tide.data.EntityProxy;
046    import org.granite.client.tide.data.Identifiable;
047    import org.granite.client.tide.data.Lazyable;
048    import org.granite.client.tide.data.PersistenceManager;
049    import org.granite.client.tide.data.RemoteInitializer;
050    import org.granite.client.tide.data.RemoteValidator;
051    import org.granite.client.tide.data.impl.UIDWeakSet.Matcher;
052    import org.granite.client.tide.data.impl.UIDWeakSet.Operation;
053    import org.granite.client.tide.data.spi.DataManager;
054    import org.granite.client.tide.data.spi.DataManager.ChangeKind;
055    import org.granite.client.tide.data.spi.DirtyCheckContext;
056    import org.granite.client.tide.data.spi.EntityDescriptor;
057    import org.granite.client.tide.data.spi.EntityRef;
058    import org.granite.client.tide.data.spi.ExpressionEvaluator;
059    import org.granite.client.tide.data.spi.ExpressionEvaluator.Value;
060    import org.granite.client.tide.data.spi.MergeContext;
061    import org.granite.client.tide.server.Component;
062    import org.granite.client.tide.server.ServerSession;
063    import org.granite.client.tide.server.TrackingContext;
064    import org.granite.client.util.WeakIdentityHashMap;
065    import org.granite.logging.Logger;
066    import org.granite.tide.Expression;
067    import org.granite.util.TypeUtil;
068    import org.granite.util.UUIDUtil;
069    
070    /**
071     * @author William DRAI
072     */
073    public class EntityManagerImpl implements EntityManager {
074        
075        private static final Logger log = Logger.getLogger(EntityManagerImpl.class);
076        
077        private String id;
078        private boolean active = false;
079        private ExpressionEvaluator expressionEvaluator = null;
080        private DataManager dataManager = null;
081        private TrackingContext trackingContext = null;
082        private DirtyCheckContext dirtyCheckContext = null;
083        private UIDWeakSet entitiesByUid = new UIDWeakSet();
084        private WeakIdentityHashMap<Object, List<Object>> entityReferences = new WeakIdentityHashMap<Object, List<Object>>();
085        
086        private DataMerger[] customMergers = null;
087        
088    
089        public EntityManagerImpl(String id, DataManager dataManager, TrackingContext trackingContext, ExpressionEvaluator expressionEvaluator) {
090            this.id = id;
091            this.active = true;
092            this.dataManager = dataManager != null ? dataManager : new JavaBeanDataManager();
093            this.dataManager.setTrackingHandler(new DefaultTrackingHandler());
094            this.trackingContext = trackingContext != null ? trackingContext : new TrackingContext();
095            this.dirtyCheckContext = new DirtyCheckContextImpl(this.dataManager, this.trackingContext);
096            this.expressionEvaluator = expressionEvaluator;
097        }
098        
099        
100        /**
101         *  Return the entity manager id
102         * 
103         *  @return the entity manager id
104         */
105        public String getId() {
106            return id;
107        }
108        
109        /**
110         *  {@inheritdoc}
111         */
112        public boolean isActive() {
113            return active;
114        }
115        
116        /**
117         *  Clear the current context
118         *  Destroys all components/context variables
119         */
120        public void clear() {
121            entitiesByUid.apply(new Operation() {
122                            @Override
123                            public void apply(Object o) {
124                                    PersistenceManager.setEntityManager(o, null);
125                            }
126            });
127            entitiesByUid.clear();
128            entityReferences.clear();
129            dirtyCheckContext.clear(false);
130            dataManager.clear();
131            trackingContext.clear();
132            active = true;
133        }
134        
135        /**
136         *  Clears entity cache
137         */ 
138        public void clearCache() {
139           // _mergeContext.clear();
140        }
141    
142        
143        public DataManager getDataManager() {
144            return dataManager;
145        }
146    
147        
148        /**
149         *  Setter for the array of custom mergers
150         * 
151         *  @param customMergers array of mergers
152         */
153        public void setCustomMergers(DataMerger[] customMergers) {
154            if (customMergers != null && customMergers.length > 0)
155                this.customMergers = customMergers;
156            else
157                this.customMergers = null;
158        }
159    
160    
161        private boolean uninitializeAllowed = true;
162        
163        @Override
164        public void setUninitializeAllowed(boolean uninitializeAllowed) {
165            this.uninitializeAllowed = uninitializeAllowed;
166        }
167    
168        @Override
169        public boolean isUninitializeAllowed() {
170            return uninitializeAllowed;
171        }
172    
173    
174        private Propagation entityManagerPropagation = null;
175            
176        /**
177         *  Setter for the propagation manager
178         * 
179         *  @param propagation propagation function that will visit child entity managers
180         */
181        public void setEntityManagerPropagation(Propagation propagation) {
182            this.entityManagerPropagation = propagation;
183        }
184        
185        /**
186         *  Setter for active flag
187         *  When EntityManager is not active, dirty checking is disabled
188         * 
189         *  @param active state
190         */
191        public void setActive(boolean active) {
192            this.active = active;
193        }
194        
195        /**
196         *  Setter for dirty check context implementation
197         * 
198         *  @param dirtyCheckContext dirty check context implementation
199         */
200        public void setDirtyCheckContext(DirtyCheckContext dirtyCheckContext) {
201            if (dirtyCheckContext == null)
202                throw new IllegalArgumentException("Dirty check context cannot be null");
203            
204    //        if (dirtyCheckContext != null)
205    //            dirtyCheckContext.removeEventListener(DIRTY_CHANGE, dirtyChangeHandler);
206                
207            this.dirtyCheckContext = dirtyCheckContext;
208            this.dirtyCheckContext.setTrackingContext(trackingContext);
209    //        this.dirtyCheckContext.addEventListener(DIRTY_CHANGE, dirtyChangeHandler, false, 0, true);
210            
211            // _mergeContext = new MergeContext(this, dirtyCheckContext);
212        }
213    
214        
215        private static int tmpEntityManagerId = 1;
216        
217        /**
218         *  Create a new temporary entity manager
219         */
220        public EntityManager newTemporaryEntityManager() {
221            return new EntityManagerImpl("$$TMP$$" + (tmpEntityManagerId++), dataManager, trackingContext, expressionEvaluator);
222        }
223    //    
224    //    /**
225    //     *  @private
226    //     *  Allow uninitialize of persistent collections
227    //     *
228    //     *  @param allowed allow uninitialize of collections
229    //     */
230    //    public void setUninitializeAllowed(boolean allowed) {
231    //        _mergeContext.uninitializeAllowed = allowed;
232    //    }
233    //    
234    //    /**
235    //     *  @private
236    //     *  @return allow uninitialize of collections
237    //     */
238    //    public function get uninitializeAllowed():Boolean {
239    //        return _mergeContext.uninitializeAllowed;
240    //    }
241    //    
242    //    /**
243    //     *  @private
244    //     *  Force uninitialize of persistent collections
245    //     * 
246    //     *  @param uninitializing force uninitializing of collections during merge
247    //     */
248    //    public function set uninitializing(uninitializing:Boolean):void {
249    //        _mergeContext.uninitializing = uninitializing;
250    //    }
251        
252        
253    //    /**
254    //     *  Entity manager is dirty when any entity/collection/map has been modified
255    //     *
256    //     *  @return is dirty
257    //     */
258    //    [Bindable(event="dirtyChange")]
259    //    public function get dirty():Boolean {
260    //        return _dirtyCheckContext.dirty;
261    //    }
262    //    
263    //    /**
264    //     *  Internal handler for dirty flag changes. Redispaches event from the dirty check context
265    //     * 
266    //     *  @param event dirty change event
267    //     */
268    //    private function dirtyChangeHandler(event:PropertyChangeEvent):void {
269    //        dispatchEvent(event);
270    //    }
271    //    
272    //    
273    //    /**
274    //     *  List of conflicts detected during last merge operation
275    //     * 
276    //     *  @return conflicts list 
277    //     */
278    //    public Conflicts getMergeConflicts() {
279    //        return _mergeContext.mergeConflicts;
280    //    }
281        
282        
283        /**
284         *  @private
285         *  Attach an entity to this context
286         * 
287         *  @param entity an entity
288         */
289        public void attachEntity(Identifiable entity) {
290            attachEntity(entity, true);
291        }
292        
293        /**
294         *  @private
295         *  Attach an entity to this context
296         * 
297         *  @param entity an entity
298         *  @param putInCache put entity in cache
299         */
300        public void attachEntity(Identifiable entity, boolean putInCache) {
301            EntityManager em = PersistenceManager.getEntityManager(entity);
302            if (em != null && em != this && !em.isActive()) {
303                throw new Error("The entity instance " + entity
304                    + " cannot be attached to two contexts (current: " + em.getId()
305                    + ", new: " + id + ")");
306            }
307            
308            PersistenceManager.setEntityManager(entity, this);
309            if (putInCache) {
310                    getUid(entity);
311                if (entitiesByUid.put(entity) == null)
312                                    dirtyCheckContext.addUnsaved(entity);
313            }
314        }
315           
316        
317        /**
318         *  @private
319         *  Detach an entity from this context only if it's not persistent
320         * 
321         *  @param entity an entity
322         *  @param removeFromCache remove entity from cache
323         */
324        public void detachEntity(Identifiable entity, boolean removeFromCache, boolean forceRemove) {
325                    if (!forceRemove) {
326                            String versionPropName = dataManager.getEntityDescriptor(entity).getVersionPropertyName();
327                            if (versionPropName == null || dataManager.getProperty(entity, versionPropName) != null)
328                                    return;
329                    }
330                    
331            dirtyCheckContext.markNotDirty(entity, entity);
332            
333            PersistenceManager.setEntityManager(entity, null);
334            if (removeFromCache)
335                entitiesByUid.remove(entity.getClass().getName() + ":" + entity.getUid());
336        }
337        
338        
339        /**
340         *  {@inheritdoc}
341         */
342        public boolean isSaved(Identifiable entity) {
343            EntityDescriptor desc = dataManager.getEntityDescriptor(entity);
344            if (desc.getVersionPropertyName() != null && dataManager.getProperty(entity, desc.getVersionPropertyName()) != null)
345                return true;
346            return false;
347        }
348        
349        
350            /**
351             *      @private
352             *  Internal implementation of object detach
353             * 
354             *  @param object object
355             *  @param cache internal cache to avoid graph loops
356             */ 
357            public void detach(Object object, IdentityHashMap<Object, Object> cache, boolean forceRemove) {
358                    if (object == null || ObjectUtil.isSimple(object))
359                            return;
360                    
361                    if (cache.containsKey(object))
362                            return;
363                    cache.put(object, object);
364                    
365                    List<String> excludes = new ArrayList<String>();
366                    excludes.add("uid");
367                    
368                    if (object instanceof Identifiable) {
369                            EntityDescriptor desc = dataManager.getEntityDescriptor(object);
370                            if (desc.getIdPropertyName() != null)
371                                    excludes.add(desc.getIdPropertyName());
372                            if (desc.getVersionPropertyName() != null)
373                                    excludes.add(desc.getVersionPropertyName());
374                    }
375                    
376                    Map<String, Object> values = dataManager.getPropertyValues(object, excludes, false, false);
377                    
378                    if (object instanceof Identifiable && entityReferences.containsKey(object)) {
379                            detachEntity((Identifiable)object, true, forceRemove);
380                            
381                            for (Entry<String, Object> me : values.entrySet())
382                                    removeReference(me.getValue(), object, me.getKey(), null);
383                    }
384                    
385                    for (Entry<String, Object> me : values.entrySet()) {
386                            Object val = me.getValue();
387                            
388                            if (val instanceof Collection && !(val instanceof LazyableCollection && !((LazyableCollection)val).isInitialized())) {
389                                    Collection<?> coll = (Collection<?>)val;
390                                    for (Object o : coll)
391                                            detach(o, cache, forceRemove);
392                            }
393                            else if (val instanceof Map<?, ?> && !(val instanceof LazyableCollection && !((LazyableCollection)val).isInitialized())) {
394                                    Map<?, ?> map = (Map<?, ?>)val;
395                                    for (Entry<?, ?> entry : map.entrySet()) {
396                                            detach(entry.getKey(), cache, forceRemove);
397                                            detach(entry.getValue(), cache, forceRemove);
398                                    }
399                            }
400                            else if (val != null && !ObjectUtil.isSimple(val)) {
401                                    detach(val, cache, forceRemove);
402                            }
403                    }
404            }
405        
406        /**
407         *  @private 
408         *  Retrives an entity in the cache from its uid
409         *   
410         *  @param object an entity
411         *  @param nullIfAbsent return null if entity not cached in context
412         */
413        public Object getCachedObject(Object object, boolean nullIfAbsent) {
414            Object entity = null;
415            if (object instanceof Identifiable) {
416                entity = entitiesByUid.get(object.getClass().getName() + ":" + getUid((Identifiable)object));
417            }
418            else if (object instanceof EntityRef) {
419                entity = entitiesByUid.get(((EntityRef)object).getClassName() + ":" + ((EntityRef)object).getUid());
420            }
421            else if (object instanceof String) {
422                    entity = entitiesByUid.get((String)object);
423            }
424    
425            if (entity != null)
426                return entity;
427            if (nullIfAbsent)
428                return null;
429    
430            return object;
431        }
432    
433        /** 
434         *  @private 
435         *  Retrives the owner entity of the provided object (collection/map/entity)
436         *   
437         *  @param object an entity
438         */
439        public Object[] getOwnerEntity(Object object) {
440            List<Object> refs = entityReferences.get(object);
441            if (refs == null)
442                return null;
443            
444            for (int i = 0; i < refs.size(); i++) {
445                if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0] instanceof String)
446                    return new Object[] { entitiesByUid.get((String)((Object[])refs.get(i))[0]), ((Object[])refs.get(i))[1] };
447            }
448            return null;
449        }
450    
451        /**
452         *  @private
453         *  Retrives the owner entity of the provided object (collection/map/entity)
454         *
455         *  @param object an entity
456         */
457        public List<Object[]> getOwnerEntities(Object object) {
458            List<Object> refs = entityReferences.get(object);
459            if (refs == null)
460                return null;
461    
462            List<Object[]> owners = new ArrayList<Object[]>();
463            for (int i = 0; i < refs.size(); i++) {
464                if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0] instanceof String) {
465                    Object owner = entitiesByUid.get((String)((Object[])refs.get(i))[0]);
466                    if (owner != null)      // May have been garbage collected
467                            owners.add(new Object[] { owner, ((Object[])refs.get(i))[1] });
468                }
469            }
470            return owners;
471        }
472    
473        
474        /**
475         *  {@inheritdoc}
476         */
477        public Expression getReference(Object obj, boolean recurse, Set<Object> cache) {
478            if (cache != null) {
479                if (cache.contains(obj))    // We are in a graph loop, no reference can be found from this path
480                    return null;
481                cache.add(obj);
482            }
483            else if (recurse)
484                throw new Error("Cache must be provided to get reference recursively");
485            
486            List<Object> refs = entityReferences.get(obj);
487            if (refs == null)
488                return null;
489            
490            for (int i = 0; i < refs.size(); i++) {
491                // Return first context expression reference that is remote enabled
492                if (refs.get(i) instanceof Expression && expressionEvaluator != null && expressionEvaluator.getRemoteSync(refs.get(i)) != SyncMode.NONE)
493                    return (Expression)refs.get(i);
494            }
495            
496            if (recurse) {
497                Object ref = null;
498                for (int i = 0; i < refs.size(); i++) {
499                    if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0] instanceof String) {
500                        ref = entitiesByUid.get((String)((Object[])refs.get(i))[0]);
501                        if (ref != null) {
502                            ref = getReference(ref, recurse, cache);
503                            if (ref != null)
504                                return (Expression)ref;
505                        }
506                    }
507                    else if (refs.get(i) instanceof Object[] && !(refs.get(i) instanceof Expression)) {
508                        ref = ((Object[])refs.get(i))[0];
509                        if (ref != null) {
510                            ref = getReference(ref, recurse, cache);
511                            if (ref != null)
512                                return (Expression)ref;
513                        } 
514                    }
515                }
516            }
517            return null;
518        }
519        
520        /**
521         *  @private
522         *  Init references array for an object
523         *   
524         *  @param obj an entity
525         */
526        private List<Object> initRefs(Object obj) {
527            List<Object> refs = entityReferences.get(obj);
528            if (refs == null) {
529                refs = new ArrayList<Object>();
530                entityReferences.put(obj, refs);
531            }
532            return refs;
533        }
534        
535    
536        /**
537         *  Registers a reference to the provided object with either a parent or res
538         * 
539         *  @param obj an entity
540         *  @param parent the parent entity
541         *  @param propName name of the parent entity property that references the entity
542         *  @param res the context expression
543         */ 
544        public void addReference(Object obj, Object parent, String propName, Expression res) {
545            if (obj instanceof Identifiable)
546                attachEntity((Identifiable)obj);
547            
548            dataManager.startTracking(obj, parent);
549    
550            if (obj instanceof ManagedPersistentAssociation)
551                obj = ((ManagedPersistentAssociation)obj).getCollection();
552            
553            List<Object> refs = entityReferences.get(obj);
554            if (!(obj instanceof LazyableCollection) && res != null) {
555                refs = initRefs(obj);
556                boolean found = false;
557                for (int i = 0; i < refs.size(); i++) {
558                    if (!(refs.get(i) instanceof Expression))
559                        continue; 
560                    Expression r = (Expression)refs.get(i);
561                    if (r.getComponentName().equals(res.getComponentName()) 
562                            && ((r.getExpression() == null && res.getExpression() == null) || (r.getExpression() != null && r.getExpression().equals(res.getExpression())))) {
563                        found = true;
564                        break;
565                    }
566                }
567                if (!found)
568                    refs.add(res);
569            }
570            boolean found = false;
571            if (parent instanceof Identifiable) {
572                String ref = parent.getClass().getName() + ":" + ((Identifiable)parent).getUid();
573                if (refs == null)
574                    refs = initRefs(obj);
575                else {
576                    for (int i = 0; i < refs.size(); i++) {
577                        if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0].equals(ref)) {
578                            found = true;
579                            break;
580                        }
581                    }
582                }
583                if (!found)
584                    refs.add(new Object[] { ref, propName });
585            }
586            else if (parent != null) {
587                if (refs == null)
588                    refs = initRefs(obj);
589                else {
590                    for (int i = 0; i < refs.size(); i++) {
591                        if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0].equals(parent)) {
592                            found = true;
593                            break;
594                        }
595                    }
596                }
597                if (!found)
598                    refs.add(new Object[] { parent, propName });
599            }
600        }
601        
602        /**
603         *  Removes a reference on the provided object
604         *
605         *  @param obj an entity
606         *  @param parent the parent entity to dereference
607         *  @param propName name of the parent entity property that references the entity
608         *  @param res expression to remove
609         */ 
610        public boolean removeReference(Object obj, Object parent, String propName, Expression res) {
611            if (obj instanceof ManagedPersistentAssociation)
612                obj = ((ManagedPersistentAssociation)obj).getCollection();
613            
614            List<Object> refs = entityReferences.get(obj);
615            if (refs == null)
616                return true;
617            
618            int idx = -1;
619            if (parent instanceof Identifiable) {
620                for (int i = 0; i < refs.size(); i++) {
621                    if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0].equals(parent.getClass().getName() + ":" + ((Identifiable)parent).getUid())) {
622                        idx = i;
623                        break;                    
624                    }
625                }
626            }
627            else if (parent != null) {
628                for (int i = 0; i < refs.size(); i++) {
629                    if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0].equals(parent)) {
630                        idx = i;
631                        break;                    
632                    }
633                }
634            }
635            else if (res != null) {
636                for (int i = 0; i < refs.size(); i++) {
637                    if (refs.get(i) instanceof Expression && ((Expression)refs.get(i)).getPath().equals(res.getPath())) {
638                        idx = i;
639                        break;
640                    }
641                }
642            }
643            if (idx >= 0)
644                refs.remove(idx);
645            
646            boolean removed = false;
647            if (refs.size() == 0) {
648                entityReferences.remove(obj);
649                removed = true;
650                
651                if (obj instanceof Identifiable)
652                    detachEntity((Identifiable)obj, true, false);
653                
654                dataManager.stopTracking(obj, parent);
655            }
656            
657            if (obj instanceof LazyableCollection && !((LazyableCollection)obj).isInitialized())
658                    return removed;
659            
660            if (obj instanceof Iterable<?>) {
661                for (Object elt : (Iterable<?>)obj)
662                    removeReference(elt, parent, propName, null);
663            }
664            else if (obj != null && obj.getClass().isArray()) {
665                for (int i = 0; i < Array.getLength(obj); i++)
666                    removeReference(Array.get(obj, i), parent, propName, null);
667            }
668            else if (obj instanceof Map<?, ?>) {
669                for (Entry<?, ?> me : ((Map<?, ?>)obj).entrySet()) {
670                    removeReference(me.getKey(), parent, propName, null);
671                    removeReference(me.getValue(), parent, propName, null);
672                }
673            }
674            
675            return removed;
676        }
677        
678        
679        public MergeContext initMerge() {
680            return new MergeContext(this, dirtyCheckContext, null);
681        }
682    
683        /**
684         *  Merge an object coming from the server in the context
685         *
686         *  @param obj external object
687         *  @param previous previously existing object in the context (null if no existing object)
688         *  @param expr current path from the context
689         *  @param parent parent object for collections
690         *  @param propertyName property name of the current object in the parent object
691         *  @param setter setter function to update the private property
692         *  @param forceUpdate force update of property (used for externalized properties)
693         *
694         *  @return merged object (should === previous when previous not null)
695         */
696        @SuppressWarnings("unchecked")
697        public Object mergeExternal(final MergeContext mergeContext, Object obj, Object previous, Expression expr, Object parent, String propertyName, String setter, boolean forceUpdate) {
698    
699            mergeContext.initMerge();
700            
701            boolean saveMergeUpdate = mergeContext.isMergeUpdate();
702            boolean saveMerging = mergeContext.isMerging();
703            
704            try {
705                mergeContext.setMerging(true);
706                int stackSize = mergeContext.getMergeStackSize();
707                
708                boolean addRef = false;
709                boolean fromCache = false;
710                Object prev = mergeContext.getFromCache(obj);
711                Object next = obj;
712                if (prev != null) {
713                    next = prev;
714                    fromCache = true;
715                }
716                else {
717                    // Give a chance to intercept received value so we can apply changes on private values
718                                    Object currentMerge = mergeContext.getCurrentMerge();
719                                    if (currentMerge instanceof EntityProxy) {
720                                            if (!((EntityProxy)currentMerge).hasProperty(propertyName))
721                                                    return previous;
722                                            next = obj = ((EntityProxy)currentMerge).getProperty(propertyName);
723                                    }
724    
725                    // Clear change tracking
726                                    dataManager.stopTracking(previous, parent);
727                                    
728                    if (obj == null) {
729                        next = null;
730                    }
731                    else if (((obj instanceof LazyableCollection && !((LazyableCollection)obj).isInitialized()) 
732                        || (obj instanceof LazyableCollection && !(previous instanceof LazyableCollection))) && parent instanceof Identifiable && propertyName != null) {
733                        next = mergePersistentCollection(mergeContext, (LazyableCollection)obj, previous, null, (Identifiable)parent, propertyName);
734                        addRef = true;
735                    }
736                    else if (obj instanceof List<?>) {
737                        next = mergeCollection(mergeContext, (List<Object>)obj, previous, parent == null ? expr : null, parent, propertyName);
738                        addRef = true;
739                    }
740                    else if (obj instanceof Map<?, ?>) {
741                        next = mergeMap(mergeContext, (Map<Object, Object>)obj, previous, parent == null ? expr : null, parent, propertyName);
742                        addRef = true;
743                    }
744    //                else if (obj instanceof Enum) {
745    //                      next = obj;
746    //                }
747                    else if (obj instanceof Identifiable) {
748                        next = mergeEntity(mergeContext, obj, previous, expr, parent, propertyName);
749                        addRef = true;
750                    }
751                    else {
752                        boolean merged = false;
753                        if (customMergers != null) {
754                            for (DataMerger merger : customMergers) {
755                                if (merger.accepts(obj)) {
756                                    next = merger.merge(mergeContext, obj, previous, parent == null ? expr : null, parent, propertyName);
757    
758                                    // Keep notified of collection updates to notify the server at next remote call
759                                    dataManager.startTracking(previous, parent);
760                                    merged = true;
761                                    addRef = true;
762                                }
763                            }
764                        }
765                        if (!merged && !ObjectUtil.isSimple(obj) && !(obj instanceof Enum || obj instanceof Value || obj instanceof byte[])) {
766                            next = mergeEntity(mergeContext, obj, previous, expr, parent, propertyName);
767                            addRef = true;
768                        }
769                    }
770                }
771                
772                if (next != null && !fromCache && addRef
773                    && (expr != null || (prev == null && parent != null))) {
774                    // Store reference from current object to its parent entity or root component expression
775                    // If it comes from the cache, we are probably in a circular graph 
776                    addReference(next, parent, propertyName, expr);
777                }
778                
779                mergeContext.setMergeUpdate(saveMergeUpdate);
780                
781                if ((mergeContext.isMergeUpdate() || forceUpdate) && setter != null && parent != null && propertyName != null && parent instanceof Identifiable && next != previous) {
782                    if (!mergeContext.isResolvingConflict() || !propertyName.equals(dataManager.getEntityDescriptor(parent).getVersionPropertyName())) {
783                        // dataManager.setInternalProperty(parent, propertyName, next);
784                        dataManager.setProperty(parent, propertyName, previous, next);
785                    }
786                }
787                
788                if (entityManagerPropagation != null && (mergeContext.isMergeUpdate() || forceUpdate) && !fromCache && obj instanceof Identifiable) {
789                    // Propagate to existing conversation contexts where the entity is present
790                    entityManagerPropagation.propagate((Identifiable)obj, new Function() {
791                        public void execute(EntityManager entityManager, Identifiable entity) {
792                            if (entityManager == mergeContext.getSourceEntityManager())
793                                return;
794                            if (entityManager.getCachedObject(entity, true) != null)
795                                entityManager.mergeFromEntityManager(entityManager, entity, mergeContext.getExternalDataSessionId(), mergeContext.isUninitializing());
796                        }
797                    });
798                }
799                
800                            if (mergeContext.getMergeStackSize() > stackSize)
801                                    mergeContext.popMerge();
802                            
803                return next;
804            }
805            catch (Exception e) {
806                    log.error(e, "Merge error");
807                    return null;
808            }
809            finally {
810                mergeContext.setMerging(saveMerging);
811            }
812        }
813    
814    
815        /**
816         *  @private 
817         *  Merge an entity coming from the server in the context
818         *
819         *  @param obj external entity
820         *  @param previous previously existing object in the context (null if no existing object)
821         *  @param expr current path from the context
822         *  @param parent parent object for collections
823         *  @param propertyName propertyName from the owner object
824         *
825         *  @return merged entity (=== previous when previous not null)
826         */ 
827        private Object mergeEntity(MergeContext mergeContext, final Object obj, Object previous, Expression expr, Object parent, String propertyName) {
828            if (obj != null || previous != null)
829                log.debug("mergeEntity: %s previous %s%s", ObjectUtil.toString(obj), ObjectUtil.toString(previous), obj == previous ? " (same)" : "");
830            
831            Object dest = obj;
832            Object p = null;
833            if (obj instanceof Lazyable && !((Lazyable)obj).isInitialized()) {
834                // If entity is uninitialized, try to lookup the cached instance by its class name and id (only works with Hibernate proxies)
835                final EntityDescriptor desc = dataManager.getEntityDescriptor(obj);
836                if (desc.getIdPropertyName() != null) {
837                    p = entitiesByUid.find(new Matcher() {
838                        public boolean match(Object o) {
839                            return o.getClass().getName().equals(obj.getClass().getName()) && 
840                                    ObjectUtil.objectEquals(dataManager, dataManager.getProperty(obj, desc.getIdPropertyName()), dataManager.getProperty(o, desc.getIdPropertyName()));
841                        }
842                    });
843    
844                    if (p != null) {
845                        previous = p;
846                        dest = previous;
847                    }
848                }
849            }
850            else if (obj instanceof Identifiable) {
851                    if (obj instanceof EntityProxy)
852                    p = entitiesByUid.get(((EntityProxy)obj).getClassName() + ":" + getUid((Identifiable)obj));
853                    else
854                            p = entitiesByUid.get(obj.getClass().getName() + ":" + getUid((Identifiable)obj));
855                if (p != null) {
856                    // Trying to merge an entity that is already cached with itself: stop now, this is not necessary to go deeper in the object graph
857                    // it should be already instrumented and tracked
858                    if (obj == p)
859                        return obj;
860                    
861                    previous = p;
862                    dest = previous;
863                }
864            }
865            
866            if (dest != previous && previous != null && (ObjectUtil.objectEquals(dataManager, previous, obj)
867                || (parent != null && !(previous instanceof Identifiable))))    // GDS-649 Case of embedded objects 
868                dest = previous;
869            
870            if (dest == obj && p == null && obj != null && mergeContext.getSourceEntityManager() != null) {
871                // When merging from another entity manager, ensure we create a new copy of the entity
872                // An instance can exist in only one entity manager at a time 
873                try {
874                    dest = TypeUtil.newInstance(obj.getClass(), Object.class);
875                    if (obj instanceof Identifiable)
876                        ((Identifiable)dest).setUid(((Identifiable)obj).getUid());
877                }
878                catch (Exception e) {
879                    throw new RuntimeException("Could not create class " + obj.getClass(), e);
880                }
881            }
882    
883            if (obj instanceof Lazyable && !((Lazyable)obj).isInitialized() && ObjectUtil.objectEquals(dataManager, previous, obj)) {
884                // Don't overwrite existing entity with an uninitialized proxy when optimistic locking is defined
885                log.debug("ignored received uninitialized proxy");
886                // Don't mark the object not dirty as we only received a proxy
887                // dirtyCheckContext.markNotDirty(previous, null);
888                return previous;
889            }
890            
891            if (dest instanceof Lazyable && !((Lazyable)dest).isInitialized())
892                log.debug("initialize lazy entity: %s", dest.toString());
893            
894            if (dest != null && dest instanceof Identifiable && dest == obj) {
895                log.debug("received entity %s used as destination (ctx: %s)", obj.toString(), this.id);
896            }
897            
898            boolean fromCache = (p != null && dest == p); 
899            
900            if (!fromCache && dest instanceof Identifiable)
901                entitiesByUid.put((Identifiable)dest);            
902            
903            mergeContext.pushMerge(obj, dest);
904            
905            boolean ignore = false;
906            if (dest instanceof Identifiable) {
907                EntityDescriptor desc = dataManager.getEntityDescriptor(dest);
908                
909                // If we are in an uninitialing temporary entity manager, try to reproxy associations when possible
910                if (mergeContext.isUninitializing() && parent instanceof Identifiable && propertyName != null) {
911                    if (desc.getVersionPropertyName() != null && dataManager.getProperty(obj, desc.getVersionPropertyName()) != null 
912                            && dataManager.getEntityDescriptor(parent).isLazy(propertyName)) {
913                        if (defineProxy(desc, dest, obj))   // Only if entity can be proxied (has a detachedState)
914                            return dest;
915                    }
916                }
917                
918                // Associate entity with the current context
919                attachEntity((Identifiable)dest, false);
920                
921                if (previous != null && dest == previous) {
922                    // Check version for optimistic locking
923                    if (desc.getVersionPropertyName() != null && !mergeContext.isResolvingConflict()) {
924                        Number newVersion = (Number)dataManager.getProperty(obj, desc.getVersionPropertyName());
925                        Number oldVersion = (Number)dataManager.getProperty(dest, desc.getVersionPropertyName());
926                        if ((newVersion != null && oldVersion != null && newVersion.longValue() < oldVersion.longValue() 
927                                || (newVersion == null && oldVersion != null))) {
928                            log.warn("ignored merge of older version of %s (current: %d, received: %d)", 
929                                dest.toString(), oldVersion, newVersion);
930                            ignore = true;
931                        }
932                        else if ((newVersion != null && oldVersion != null && newVersion.longValue() > oldVersion.longValue()) 
933                                || (newVersion != null && oldVersion == null)) {
934                            // Handle changes when version number is increased
935                            mergeContext.markVersionChanged(dest);
936                            
937                                                    boolean entityChanged = dirtyCheckContext.isEntityChanged((Identifiable)dest);
938                                    if (mergeContext.getExternalDataSessionId() != null && entityChanged) {
939                                // Conflict between externally received data and local modifications
940                                log.error("conflict with external data detected on %s (current: %d, received: %d)",
941                                    dest.toString(), oldVersion, newVersion);
942                                
943                                // Check incoming values and local values
944                                if (dirtyCheckContext.checkAndMarkNotDirty(mergeContext, dest, obj, null)) {
945                                    // Incoming data is different from local data
946                                    Map<String, Object> save = dirtyCheckContext.getSavedProperties(dest);
947                                    List<String> properties = new ArrayList<String>(save.keySet());
948                                    properties.remove(desc.getVersionPropertyName());
949                                    Collections.sort(properties);
950                                    
951                                    mergeContext.addConflict((Identifiable)dest, (Identifiable)obj, properties);
952                                    
953                                    ignore = true;
954                                }
955                                else
956                                    mergeContext.setMergeUpdate(true);
957                            }
958                            else
959                                mergeContext.setMergeUpdate(true);
960                        }
961                        else {
962                            // Data has been changed locally and not persisted, don't overwrite when version number is unchanged
963                            if (dirtyCheckContext.isEntityChanged((Identifiable)dest))
964                                mergeContext.setMergeUpdate(false);
965                            else
966                                mergeContext.setMergeUpdate(true);
967                        }
968                    }
969                    else if (!mergeContext.isResolvingConflict())
970                        mergeContext.markVersionChanged(dest);
971                }
972                else
973                    mergeContext.markVersionChanged(dest);
974                
975                if (!ignore) {
976                                    if (obj instanceof EntityProxy) {
977                                            mergeContext.setCurrentMerge(obj);
978                                            defaultMerge(mergeContext, ((EntityProxy)obj).getWrappedObject(), dest, expr, parent, propertyName);
979                                    }
980                                    else
981                                            defaultMerge(mergeContext, obj, dest, expr, parent, propertyName);
982                }
983            }
984            else
985                defaultMerge(mergeContext, obj, dest, expr, parent, propertyName);
986            
987            if (dest != null && !ignore && !mergeContext.isSkipDirtyCheck() && !mergeContext.isResolvingConflict())
988                dirtyCheckContext.checkAndMarkNotDirty(mergeContext, dest, obj, (parent instanceof Identifiable && !(dest instanceof Identifiable)) ? parent : null);
989            
990            if (dest != null)
991                log.debug("mergeEntity result: %s", dest.toString());
992            
993            // Keep notified of collection updates to notify the server at next remote call
994            dataManager.startTracking(dest, parent);
995            
996            return dest;
997        }
998        
999        
1000        private boolean defineProxy(EntityDescriptor desc, Object dest, Object obj) {
1001            if (desc.getDetachedStateField() == null)
1002                return false;
1003    
1004            try {
1005                if (obj != null) {
1006                    if (desc.getDetachedStateField().get(obj) == null)
1007                        return false;
1008                    dataManager.setProperty(dest, desc.getIdPropertyName(), null, 
1009                            dataManager.getProperty(obj, desc.getIdPropertyName()));
1010                    desc.getDetachedStateField().set(dest, desc.getDetachedStateField().get(obj));
1011                }
1012                desc.getInitializedField().set(dest, false);
1013                return true;
1014            }
1015            catch (Exception e) {
1016                throw new RuntimeException("Could not proxy class " + obj.getClass());
1017            }
1018        }
1019        
1020    
1021        /**
1022         *  @private 
1023         *  Merge a collection coming from the server in the context
1024         *
1025         *  @param coll external collection
1026         *  @param previous previously existing collection in the context (can be null if no existing collection)
1027         *  @param expr current path from the context
1028         *  @param parent owner object for collections
1029         *  @param propertyName property name in owner object
1030         * 
1031         *  @return merged collection (=== previous when previous not null)
1032         */ 
1033        @SuppressWarnings("unchecked")
1034        private List<?> mergeCollection(MergeContext mergeContext, List<Object> coll, Object previous, Expression expr, Object parent, String propertyName) {
1035            log.debug("mergeCollection: %s previous %s", ObjectUtil.toString(coll), ObjectUtil.toString(previous));
1036            
1037            if (mergeContext.isUninitializing() && parent instanceof Identifiable && propertyName != null) {
1038                    EntityDescriptor desc = dataManager.getEntityDescriptor(parent);
1039                    if (desc.getVersionPropertyName() != null && dataManager.getProperty(parent, desc.getVersionPropertyName()) != null
1040                            && desc.isLazy(propertyName) && previous instanceof LazyableCollection && ((LazyableCollection)previous).isInitialized()) {
1041                    log.debug("uninitialize lazy collection %s", ObjectUtil.toString(previous));
1042                    mergeContext.pushMerge(coll, previous);
1043                    
1044                    ((LazyableCollection)previous).uninitialize();
1045                    return (List<?>)previous;
1046                }
1047            }
1048    
1049            if (previous != null && previous instanceof LazyableCollection && !((LazyableCollection)previous).isInitialized()) {
1050                log.debug("initialize lazy collection %s", ObjectUtil.toString(previous));
1051                mergeContext.pushMerge(coll, previous);
1052                
1053                ((LazyableCollection)previous).initializing();
1054                
1055                List<Object> added = new ArrayList<Object>(coll.size());
1056                for (int i = 0; i < coll.size(); i++) {
1057                    Object obj = coll.get(i);
1058    
1059                    obj = mergeExternal(mergeContext, obj, null, null, propertyName != null ? parent : null, propertyName, null, false);
1060                    added.add(obj);
1061                }
1062                
1063                ((LazyableCollection)previous).initialize();
1064                ((Collection<Object>)previous).addAll(added);
1065    
1066                // Keep notified of collection updates to notify the server at next remote call
1067                dataManager.startTracking(previous, parent);
1068    
1069                return (List<?>)previous;
1070            }
1071    
1072            boolean tracking = false;
1073            
1074            List<?> nextList = null;
1075            List<Object> list = null;
1076            if (previous != null && previous instanceof List<?>)
1077                list = (List<Object>)previous;
1078            else if (mergeContext.getSourceEntityManager() != null) {
1079                try {
1080                    list = coll.getClass().newInstance();
1081                }
1082                catch (Exception e) {
1083                    throw new RuntimeException("Could not create class " + coll.getClass());
1084                }
1085            }
1086            else
1087                list = (List<Object>)coll;
1088                            
1089            mergeContext.pushMerge(coll, list);
1090    
1091            List<Object> prevColl = list != coll ? list : null;
1092            List<Object> destColl = prevColl;
1093    
1094            if (prevColl != null && mergeContext.isMergeUpdate()) {
1095                // Enable tracking before modifying collection when resolving a conflict
1096                // so the dirty checking can save changes
1097                if (mergeContext.isResolvingConflict()) {
1098                    dataManager.startTracking(prevColl, parent);
1099                    tracking = true;
1100                }
1101                
1102                for (int i = 0; i < destColl.size(); i++) {
1103                    Object obj = destColl.get(i);
1104                    boolean found = false;
1105                    for (int j = 0; j < coll.size(); j++) {
1106                        Object next = coll.get(j);
1107                        if (ObjectUtil.objectEquals(dataManager, next, obj)) {
1108                            found = true;
1109                            break;
1110                        }
1111                    }
1112                    if (!found) {
1113                        destColl.remove(i);
1114                        i--;
1115                    }
1116                }
1117            }
1118            for (int i = 0; i < coll.size(); i++) {
1119                Object obj = coll.get(i);
1120                if (destColl != null) {
1121                    boolean found = false;
1122                    for (int j = i; j < destColl.size(); j++) {
1123                        Object prev = destColl.get(j);
1124                        if (i < destColl.size() && ObjectUtil.objectEquals(dataManager, prev, obj)) {
1125                            obj = mergeExternal(mergeContext, obj, prev, propertyName != null ? expr : null, propertyName != null ? parent : null, propertyName, null, false);
1126                            
1127                            if (j != i) {
1128                                destColl.remove(j);
1129                                if (i < destColl.size())
1130                                    destColl.add(i, obj);
1131                                else
1132                                    destColl.add(obj);
1133                                if (i > j)
1134                                    j--;
1135                            }
1136                            else if (obj != prev)
1137                                destColl.set(i, obj);
1138                            
1139                            found = true;
1140                        }
1141                    }
1142                    if (!found) {
1143                        obj = mergeExternal(mergeContext, obj, null, propertyName != null ? expr : null, propertyName != null ? parent : null, propertyName, null, false);
1144                        
1145                        if (mergeContext.isMergeUpdate()) {
1146                            if (i < prevColl.size())
1147                                destColl.add(i, obj);
1148                            else
1149                                destColl.add(obj);
1150                        }
1151                    }
1152                }
1153                else {
1154                    Object prev = obj;
1155                    obj = mergeExternal(mergeContext, obj, null, propertyName != null ? expr : null, propertyName != null ? parent : null, propertyName, null, false);
1156                    if (obj != prev)
1157                        coll.set(i, obj);
1158                }
1159            }
1160            if (destColl != null && mergeContext.isMergeUpdate()) {
1161                if (!mergeContext.isResolvingConflict() && !mergeContext.isSkipDirtyCheck())
1162                    dirtyCheckContext.markNotDirty(previous, (Identifiable)parent);
1163                
1164                nextList = prevColl;
1165            }
1166            else if (prevColl instanceof LazyableCollection && !mergeContext.isMergeUpdate()) {
1167                            nextList = prevColl;
1168                    }
1169            else
1170                nextList = coll;
1171            
1172            // Wrap persistent collections
1173            if (parent instanceof Identifiable && propertyName != null && nextList instanceof LazyableCollection && !(nextList instanceof ManagedPersistentCollection)) {
1174                log.debug("create initialized persistent collection from %s", ObjectUtil.toString(nextList));
1175                
1176                nextList = dataManager.newPersistentCollection((Identifiable)parent, propertyName, (LazyableCollection)nextList);
1177            }
1178            else
1179                log.debug("mergeCollection result: %s", ObjectUtil.toString(nextList));
1180            
1181            mergeContext.pushMerge(coll, nextList, false);
1182            
1183            if (!tracking)
1184                dataManager.startTracking(nextList, parent);
1185    
1186            return nextList;
1187        }
1188    
1189        /**
1190         *  @private 
1191         *  Merge a map coming from the server in the context
1192         *
1193         *  @param map external map
1194         *  @param previous previously existing map in the context (null if no existing map)
1195         *  @param expr current path from the context
1196         *  @param parent owner object for the map if applicable
1197         * 
1198         *  @return merged map (=== previous when previous not null)
1199         */ 
1200        @SuppressWarnings("unchecked")
1201        private Map<?, ?> mergeMap(MergeContext mergeContext, Map<Object, Object> map, Object previous, Expression expr, Object parent, String propertyName) {
1202            log.debug("mergeMap: %s previous %s", ObjectUtil.toString(map), ObjectUtil.toString(previous));
1203            
1204            if (mergeContext.isUninitializing() && parent instanceof Identifiable && propertyName != null) {
1205                    EntityDescriptor desc = dataManager.getEntityDescriptor(parent);
1206                    if (desc.getVersionPropertyName() != null && dataManager.getProperty(parent, desc.getVersionPropertyName()) != null
1207                            && desc.isLazy(propertyName) && previous instanceof LazyableCollection && ((LazyableCollection)previous).isInitialized()) {
1208                    log.debug("uninitialize lazy map %s", ObjectUtil.toString(previous));
1209                    
1210                    mergeContext.pushMerge(map, previous);
1211                    ((LazyableCollection)previous).uninitialize();
1212                    return (Map<?, ?>)previous;
1213                }
1214            }
1215    
1216            if (previous != null && previous instanceof LazyableCollection && !((LazyableCollection)previous).isInitialized()) {
1217                log.debug("initialize lazy map %s", ObjectUtil.toString(previous));
1218                mergeContext.pushMerge(map, previous);
1219                
1220                ((LazyableCollection)previous).initializing();
1221                
1222                for (Entry<?, ?> me : map.entrySet()) {
1223                    Object key = mergeExternal(mergeContext, me.getKey(), null, null, propertyName != null ? parent: null, propertyName, null, false);
1224                    Object value = mergeExternal(mergeContext, me.getValue(), null, null, propertyName != null ? parent : null, propertyName, null, false);
1225                    ((Map<Object, Object>)previous).put(key, value);
1226                }
1227                
1228                ((LazyableCollection)previous).initialize();
1229    
1230                // Keep notified of collection updates to notify the server at next remote call
1231                dataManager.startTracking(previous, parent);
1232    
1233                return (Map<?, ?>)previous;
1234            }
1235            
1236            boolean tracking = false;
1237            
1238            Map<Object, Object> nextMap = null;
1239            Map<Object, Object> m = null;
1240            if (previous != null && previous instanceof Map<?, ?>)
1241                m = (Map<Object, Object>)previous;
1242            else if (mergeContext.getSourceEntityManager() != null) {
1243                try {
1244                    m = (Map<Object, Object>)TypeUtil.newInstance(map.getClass(), Map.class);
1245                }
1246                catch (Exception e) {
1247                    throw new RuntimeException("Could not create class " + map.getClass());
1248                }
1249            }
1250            else
1251                m = map;
1252            mergeContext.pushMerge(map, m);
1253            
1254            Map<Object, Object> prevMap = m != map ? m : null;
1255            
1256            if (prevMap != null) {
1257                if (mergeContext.isResolvingConflict()) {
1258                    dataManager.startTracking(prevMap, parent);
1259                    tracking = true;
1260                }
1261                
1262                if (map != prevMap) {
1263                    for (Entry<?, ?> me : map.entrySet()) {
1264                        Object newKey = mergeExternal(mergeContext, me.getKey(), null, null, parent, propertyName, null, false);
1265                        Object prevValue = prevMap.get(newKey);
1266                        Object value = mergeExternal(mergeContext, me.getValue(), prevValue, null, parent, propertyName, null, false);
1267                        if (mergeContext.isMergeUpdate() || prevMap.containsKey(newKey))
1268                            prevMap.put(newKey, value);
1269                    }
1270                    
1271                    if (mergeContext.isMergeUpdate()) {
1272                        Iterator<Object> imap = prevMap.keySet().iterator();
1273                        while (imap.hasNext()) {
1274                            Object key = imap.next();
1275                            boolean found = false;
1276                            for (Object k : map.keySet()) {
1277                                if (ObjectUtil.objectEquals(dataManager, k, key)) {
1278                                    found = true;
1279                                    break;
1280                                }
1281                            }
1282                            if (!found)
1283                                imap.remove();
1284                        }
1285                    }
1286                }
1287                
1288                if (mergeContext.isMergeUpdate() && !mergeContext.isResolvingConflict() && !mergeContext.isSkipDirtyCheck())
1289                    dirtyCheckContext.markNotDirty(previous, (Identifiable)parent);
1290                
1291                nextMap = prevMap;
1292            }
1293            else {
1294                List<Object[]> addedToMap = new ArrayList<Object[]>();
1295                for (Entry<?, ?> me : map.entrySet()) {
1296                    Object value = mergeExternal(mergeContext, me.getValue(), null, null, parent, propertyName, null, false);
1297                    Object key = mergeExternal(mergeContext, me.getKey(), null, null, parent, propertyName, null, false);
1298                    addedToMap.add(new Object[] { key, value });
1299                }
1300                map.clear();
1301                for (Object[] obj : addedToMap)
1302                    map.put(obj[0], obj[1]);
1303                
1304                nextMap = map;
1305            }
1306                
1307            if (parent instanceof Identifiable && propertyName != null && nextMap instanceof LazyableCollection && !(nextMap instanceof ManagedPersistentMap)) {
1308                log.debug("create initialized persistent map from %s", ObjectUtil.toString(nextMap));
1309                
1310                nextMap = dataManager.newPersistentMap((Identifiable)parent, propertyName, (LazyableCollection)nextMap);
1311            }
1312            else
1313                log.debug("mergeMap result: %s", ObjectUtil.toString(nextMap));
1314            
1315            mergeContext.pushMerge(map, nextMap, false);
1316            
1317            if (!tracking)
1318                dataManager.startTracking(nextMap, parent);
1319            
1320            return nextMap;
1321        } 
1322    
1323    
1324        /**
1325         *  @private 
1326         *  Wraps a persistent collection to manage lazy initialization
1327         *
1328         *  @param coll the collection to wrap
1329         *  @param previous the previous existing collection
1330         *  @param expr the path expression from the context
1331         *  @param parent the owner object
1332         *  @param propertyName owner property
1333         * 
1334         *  @return the wrapped persistent collection
1335         */ 
1336        protected Object mergePersistentCollection(MergeContext mergeContext, LazyableCollection coll, Object previous, Expression expr, Identifiable parent, String propertyName) {
1337            if (previous instanceof ManagedPersistentCollection<?>) {
1338                mergeContext.pushMerge(coll, previous);
1339                if (((LazyableCollection)previous).isInitialized()) {
1340                    if (mergeContext.isUninitializeAllowed() && mergeContext.hasVersionChanged(parent)) {
1341                        log.debug("uninitialize lazy collection %s", ObjectUtil.toString(previous));
1342                        ((LazyableCollection)previous).uninitialize();
1343                    }
1344                    else
1345                        log.debug("keep initialized collection %s", ObjectUtil.toString(previous));
1346                }
1347                dataManager.startTracking(previous, parent);
1348                return previous;
1349            }
1350            else if (previous instanceof ManagedPersistentMap<?, ?>) {
1351                mergeContext.pushMerge(coll, previous);
1352                if (((LazyableCollection)previous).isInitialized()) {
1353                    if (mergeContext.isUninitializeAllowed() && mergeContext.hasVersionChanged(parent)) {
1354                        log.debug("uninitialize lazy map %s", ObjectUtil.toString(previous));
1355                        ((LazyableCollection)previous).uninitialize();
1356                    }
1357                    else
1358                        log.debug("keep initialized map %s", ObjectUtil.toString(previous));
1359                }
1360                dataManager.startTracking(previous, parent);
1361                return previous;
1362            }
1363            
1364            if (coll instanceof Map<?, ?>) {
1365                            LazyableCollection pm = (LazyableCollection)coll;
1366                            if (previous instanceof LazyableCollection)
1367                                    pm = (LazyableCollection)previous;
1368                            if (coll instanceof ManagedPersistentMap<?, ?>)
1369                                    pm = ((ManagedPersistentMap<?, ?>)coll).clone(mergeContext.isUninitializing());
1370                            else if (mergeContext.getSourceEntityManager() != null)
1371                                    pm = pm.clone(mergeContext.isUninitializing());
1372                            
1373                    ManagedPersistentMap<Object, Object> pmap = dataManager.newPersistentMap(parent, propertyName, pm);
1374                pmap.setServerSession(mergeContext.getServerSession());
1375                mergeContext.pushMerge(coll, pmap);
1376                
1377                if (pmap.isInitialized()) {
1378                    List<Object> keys = new ArrayList<Object>(pmap.keySet());
1379                    for (Object key : keys) {
1380                        Object value = pmap.remove(key);
1381                        key = mergeExternal(mergeContext, key, null, null, parent, propertyName, null, false);
1382                        value = mergeExternal(mergeContext, value, null, null, parent, propertyName, null, false);
1383                        pmap.put(key, value);
1384                    }
1385                    dataManager.startTracking(pmap, parent);
1386                }
1387                else if (parent instanceof Identifiable && propertyName != null)
1388                    dataManager.getEntityDescriptor(parent).setLazy(propertyName);
1389                return pmap;
1390            }
1391            
1392                    LazyableCollection pc = (LazyableCollection)coll;
1393                    if (previous instanceof LazyableCollection)
1394                            pc = (LazyableCollection)previous;
1395                    if (coll instanceof ManagedPersistentCollection<?>)
1396                            pc = duplicatePersistentCollection(mergeContext, ((ManagedPersistentCollection<?>)coll).getCollection(), parent, propertyName);
1397                    else if (mergeContext.getSourceEntityManager() != null)
1398                            pc = duplicatePersistentCollection(mergeContext, pc, parent, propertyName);
1399                    
1400            ManagedPersistentCollection<Object> pcoll = dataManager.newPersistentCollection(parent, propertyName, pc);
1401            pcoll.setServerSession(mergeContext.getServerSession());
1402            mergeContext.pushMerge(coll, pcoll);
1403            
1404            if (pcoll.isInitialized()) {
1405                for (int i = 0; i < pcoll.size(); i++) {
1406                    Object obj = mergeExternal(mergeContext, pcoll.get(i), null, null, parent, propertyName, null, false);
1407                    if (obj != pcoll.get(i)) 
1408                        pcoll.set(i, obj);
1409                }
1410                dataManager.startTracking(pcoll, parent);
1411            }
1412            else if (parent instanceof Identifiable && propertyName != null)
1413                dataManager.getEntityDescriptor(parent).setLazy(propertyName);
1414            return pcoll;
1415        }
1416        
1417        private LazyableCollection duplicatePersistentCollection(MergeContext mergeContext, Object coll, Object parent, String propertyName) {
1418            if (!(coll instanceof LazyableCollection))
1419                            throw new RuntimeException("Not a persistent collection/map " + ObjectUtil.toString(coll));
1420                    
1421                    LazyableCollection ccoll = ((LazyableCollection)coll).clone(mergeContext.isUninitializing());
1422                    
1423                    if (mergeContext.isUninitializing() && parent != null && propertyName != null) {
1424                            EntityDescriptor desc = dataManager.getEntityDescriptor((Identifiable)parent);
1425                            if (desc.getVersionPropertyName() != null && dataManager.getProperty(parent, desc.getVersionPropertyName()) != null && desc.isLazy(propertyName))
1426                                    ccoll.uninitialize();
1427                    }
1428                    return ccoll;
1429        }
1430        
1431        
1432        /**
1433         *  @private 
1434         *  Merge an object coming from another entity manager (in general in the global context) in the local context
1435         *
1436         *  @param sourceEntityManager source context of incoming data
1437         *  @param obj external object
1438         *  @param externalDataSessionId is merge from external data
1439         *  @param uninitializing true to force folding of loaded lazy associations
1440         *
1441         *  @return merged object
1442         */
1443        public Object mergeFromEntityManager(EntityManager sourceEntityManager, Object obj, String externalDataSessionId, boolean uninitializing) {
1444            try {
1445                MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, null);
1446                mergeContext.setSourceEntityManager(sourceEntityManager);
1447                mergeContext.setUninitializing(uninitializing);
1448                mergeContext.setExternalDataSessionId(externalDataSessionId);        
1449                
1450                Object next = externalDataSessionId != null
1451                    ? internalMergeExternalData(mergeContext, obj, null, null, null) // Force handling of external data
1452                    : mergeExternal(mergeContext, obj, null, null, null, null, null, false);
1453                
1454                return next;
1455            }
1456            finally {
1457                MergeContext.destroy(this);
1458            }
1459        }
1460        
1461        
1462        /**
1463         *  @private 
1464         *  Merge an object coming from a remote location (in general from a service) in the local context
1465         *
1466         *  @param obj external object
1467         *
1468         *  @return merged object (should === previous when previous not null)
1469         */
1470    
1471        public Object mergeExternalData(Object obj) {
1472            return mergeExternalData(null, obj, null, null, null, null);
1473        }
1474        
1475        public Object mergeExternalData(ServerSession serverSession, Object obj) {
1476            return mergeExternalData(serverSession, obj, null, null, null, null);
1477        }
1478        
1479        public Object mergeExternalData(Object obj, Object prev, String externalDataSessionId, List<Object> removals, List<Object> persists) {
1480            return mergeExternalData(null, obj, prev, externalDataSessionId, removals, persists);
1481        }
1482        
1483        /**
1484         *  @private 
1485         *  Merge an object coming from a remote location (in general from a service) in the local context
1486         *
1487         *  @param obj external object
1488         *  @param prev existing local object to merge with
1489         *  @param externalDataSessionId sessionId from which the data is coming (other user/server), null if local or current user session
1490         *  @param removals array of entities to remove from the entity manager cache
1491         *
1492         *  @return merged object (should === previous when previous not null)
1493         */
1494        public Object mergeExternalData(ServerSession serverSession, Object obj, Object prev, String externalDataSessionId, List<Object> removals, List<Object> persists) {
1495            try {
1496                MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, null);
1497                mergeContext.setServerSession(serverSession);
1498                mergeContext.setExternalDataSessionId(externalDataSessionId);
1499                
1500                return internalMergeExternalData(mergeContext, obj, prev, removals, persists);
1501            }
1502            finally {
1503                MergeContext.destroy(this);
1504            }
1505        }
1506        
1507        /**
1508         *  @private 
1509         *  Merge an object coming from a remote location (in general from a service) in the local context
1510         *
1511         *  @param obj external object
1512         *  @param prev existing local object to merge with
1513         *  @param externalDataSessionId sessionId from which the data is coming (other user/server), null if local or current user session
1514         *  @param removals array of entities to remove from the entity manager cache
1515         *
1516         *  @return merged object (should === previous when previous not null)
1517         */
1518        public Object internalMergeExternalData(MergeContext mergeContext, Object obj, Object prev, List<Object> removals, List<Object> persists) {
1519            Map<String, Object> savedContext = null;
1520            
1521            try {
1522                if (mergeContext.getExternalDataSessionId() != null)
1523                    savedContext = trackingContext.saveAndResetContext();
1524                
1525                Object next = mergeExternal(mergeContext, obj, prev, null, null, null, null, false);
1526                
1527                if (removals != null)
1528                    handleRemovalsAndPersists(mergeContext, removals, persists);
1529                
1530                if (mergeContext.getExternalDataSessionId() != null) {
1531                    handleMergeConflicts(mergeContext);         
1532                    clearCache();
1533                }
1534                
1535                return next;
1536            }
1537            finally {               
1538                if (mergeContext.getExternalDataSessionId() != null)
1539                    trackingContext.restoreContext(savedContext);
1540            }           
1541        }
1542        
1543        
1544        /**
1545         *  Merge conversation entity manager context variables in global entity manager 
1546         *  Only applicable to conversation contexts 
1547         * 
1548         *  @param entityManager conversation entity manager
1549         */
1550        public void mergeInEntityManager(final EntityManager entityManager) {
1551            final Set<Object> cache = new HashSet<Object>();
1552            final EntityManager sourceEntityManager = this;
1553            entitiesByUid.apply(new UIDWeakSet.Operation() {
1554                public void apply(Object obj) {
1555                    // Reset local dirty state, only server state can safely be merged in global context
1556                    if (obj instanceof Identifiable)
1557                        resetEntity((Identifiable)obj, cache);
1558                    entityManager.mergeFromEntityManager(sourceEntityManager, obj, null, false);
1559                }
1560            });
1561        }
1562    
1563    
1564        @Override
1565        public boolean isDirty() {
1566            return dataManager.isDirty();
1567        }
1568        
1569        public boolean isDeepDirtyEntity(Object entity) {
1570            return dirtyCheckContext.isEntityDeepChanged(entity);
1571        }
1572    
1573        @Override
1574        public boolean isSaved(Object entity) {
1575            return dirtyCheckContext.getSavedProperties(entity) != null;
1576        }
1577        
1578        
1579        private String getUid(Identifiable uidObject) {
1580            String uid = uidObject.getUid();
1581            if (uid == null) {
1582                    uid = UUIDUtil.randomUUID();
1583                    uidObject.setUid(uid);
1584            }
1585            return uid;
1586        }
1587    
1588        
1589        /**
1590         *  Remove elements from cache and managed collections
1591         *
1592         *  @param removals array of entity instances to remove from the entity manager cache
1593         */
1594        public void handleRemovalsAndPersists(MergeContext mergeContext, List<Object> removals, List<Object> persists) {
1595            for (Object removal : removals) {
1596                Object entity = getCachedObject(removal, true);
1597                if (entity == null) // Not found in local cache, cannot remove
1598                    continue;
1599    
1600                if (mergeContext.getExternalDataSessionId() != null && !mergeContext.isResolvingConflict() 
1601                        && dirtyCheckContext.isEntityChanged(entity)) {
1602                    // Conflict between externally received data and local modifications
1603                    log.error("conflict with external data removal detected on %s", ObjectUtil.toString(entity));
1604    
1605                    mergeContext.addConflict((Identifiable)entity, null, null);
1606                }
1607                else {
1608                    boolean saveMerging = mergeContext.isMerging();
1609                    try {
1610                            mergeContext.setMerging(true);
1611                                    
1612                            List<Object[]> owners = getOwnerEntities(entity);
1613                            if (owners != null) {
1614                                for (Object[] owner : owners) {
1615                                    Object val = dataManager.getProperty(owner[0], (String)owner[1]);
1616                                    if (val instanceof LazyableCollection && !((LazyableCollection)val).isInitialized())
1617                                        continue;
1618                                    if (val instanceof List<?>) {
1619                                        int idx = ((List<?>)val).indexOf(entity);
1620                                        if (idx >= 0)
1621                                            ((List<?>)val).remove(idx);
1622                                    }
1623                                    else if (val instanceof Map<?, ?>) {
1624                                        Map<?, ?> map = (Map<?, ?>)val;
1625                                        if (map.containsKey(entity))
1626                                            map.remove(entity);
1627            
1628                                        for (Iterator<?> ikey = map.keySet().iterator(); ikey.hasNext(); ) {
1629                                            Object key = ikey.next();
1630                                            if (ObjectUtil.objectEquals(dataManager, map.get(key), entity))
1631                                                ikey.remove();
1632                                        }
1633                                    }
1634                                }
1635                            }
1636                            
1637                            /* May not be necessary, should be cleaned up by weak reference */
1638                            Map<String, Object> pvalues = dataManager.getPropertyValues(entity, false, true);
1639                            for (Object val : pvalues.values()) {
1640                                if (val instanceof List<?> || val instanceof Map<?, ?> || (val != null && val.getClass().isArray()))
1641                                    entityReferences.remove(val);
1642                            }
1643                            entityReferences.remove(entity);
1644                            
1645                            detach((Identifiable)entity, new IdentityHashMap<Object, Object>(), true);
1646                    }
1647                                    finally {
1648                                            mergeContext.setMerging(saveMerging);
1649                                    }
1650                }
1651            }
1652                    
1653                    dirtyCheckContext.fixRemovalsAndPersists(mergeContext, removals, persists);
1654        }
1655        
1656        
1657        private List<DataConflictListener> dataConflictListeners = new ArrayList<DataConflictListener>();
1658        
1659        public void addListener(DataConflictListener listener) {
1660            dataConflictListeners.add(listener);
1661        }
1662        
1663        public void removeListener(DataConflictListener listener) {
1664            dataConflictListeners.remove(listener);
1665        }
1666    
1667        /**
1668         *  Dispatch an event when last merge generated conflicts 
1669         */
1670        public void handleMergeConflicts(MergeContext mergeContext) {
1671            // Clear thread cache so acceptClient/acceptServer can work inside the conflicts handler
1672            // mergeContext.clearCache();
1673            mergeContext.initMergeConflicts();
1674    
1675            if (mergeContext.getMergeConflicts() != null) {
1676                    for (DataConflictListener listener : dataConflictListeners)
1677                        listener.onConflict(this, mergeContext.getMergeConflicts());
1678            }
1679        }
1680        
1681        /**
1682         *  Resolve merge conflicts
1683         * 
1684         *  @param modifiedEntity the received entity
1685         *  @param localEntity the locally cached entity
1686         *  @param resolving true to keep client state
1687         */
1688        public void resolveMergeConflicts(MergeContext mergeContext, Object modifiedEntity, Object localEntity, boolean resolving) {
1689            try {
1690                mergeContext.setResolvingConflict(resolving);
1691                
1692                if (modifiedEntity == null)
1693                    handleRemovalsAndPersists(mergeContext, Collections.singletonList(localEntity), Collections.emptyList());
1694                else
1695                    mergeExternal(mergeContext, modifiedEntity, localEntity, null, null, null, null, false);
1696        
1697                mergeContext.checkConflictsResolved();
1698            }
1699            finally {
1700                mergeContext.setResolvingConflict(false);
1701            }
1702        }
1703        
1704        
1705    //    /**
1706    //     *  Enables or disabled dirty checking in this context
1707    //     *  
1708    //     *  @param enabled
1709    //     */
1710    //    public void setDirtyCheckEnabled(boolean enabled) {
1711    //        _mergeContext.merging = !enabled;
1712    //    }
1713        
1714        
1715        /**
1716         *  {@inheritDoc}
1717         */
1718        public Map<String, Object> getSavedProperties(Object entity) {
1719            Object localEntity = getCachedObject(entity, true);
1720            if (localEntity == null)
1721                return null;
1722            return dirtyCheckContext.getSavedProperties(localEntity);
1723        }
1724        
1725        
1726        /**
1727         *  Default implementation of entity merge for simple ActionScript beans with public properties
1728         *  Can be used to implement Tide managed entities with simple objects
1729         *
1730         *  @param em the context
1731         *  @param obj source object
1732         *  @param dest destination object
1733         *  @param expr current path of the entity in the context (mostly for internal use)
1734         *  @param parent owning object
1735         *  @param propertyName property name of the owning object
1736         */ 
1737        public void defaultMerge(MergeContext mergeContext, Object obj, Object dest, Expression expr, Object parent, String propertyName) {
1738            // Merge internal state
1739            try {
1740                EntityDescriptor desc = dataManager.getEntityDescriptor(obj);
1741                if (desc.getInitializedField() != null)
1742                    desc.getInitializedField().set(dest, desc.getInitializedField().get(obj));
1743                if (desc.getDetachedStateField() != null)
1744                    desc.getDetachedStateField().set(dest, desc.getDetachedStateField().get(obj));
1745            }
1746            catch (Exception e) {
1747                log.error(e, "Could not merge internal state of object " + ObjectUtil.toString(obj));
1748            }
1749            
1750            Map<String, Object> pval = dataManager.getPropertyValues(obj, false, false);
1751            List<String> rw = new ArrayList<String>();
1752            
1753            boolean isEmbedded = parent instanceof Identifiable && !(obj instanceof Identifiable);
1754            for (Entry<String, Object> mval : pval.entrySet()) {
1755                String propName = mval.getKey();
1756                Object o = mval.getValue();
1757                Object d = dataManager.getProperty(dest, propName);
1758                o = mergeExternal(mergeContext, o, d, expr, isEmbedded ? parent : dest, isEmbedded ? propertyName + "." + propName : propName, propName, false);
1759                if (o != d && mergeContext.isMergeUpdate())
1760                    dataManager.setInternalProperty(dest, propName, o);
1761                rw.add(propName);
1762            }
1763            
1764            pval = dataManager.getPropertyValues(obj, rw, true, false);
1765            for (Entry<String, Object> mval : pval.entrySet()) {
1766                String propName = mval.getKey();
1767                Object o = mval.getValue();
1768                Object d = dataManager.getProperty(dest, propName);
1769                if (o instanceof Identifiable || d instanceof Identifiable)
1770                    throw new IllegalStateException("Cannot merge the read-only property " + propName + " on bean " + obj + " with an Identifiable value, this will break local unicity and caching. Change property access to read-write.");  
1771                
1772                mergeExternal(mergeContext, o, d, expr, parent != null ? parent : dest, propertyName != null ? propertyName + '.' + propName : propName, null, false);
1773            }
1774        }
1775        
1776            
1777        public boolean isEntityChanged(Identifiable entity) {
1778            return dirtyCheckContext.isEntityChanged(entity);
1779        }
1780        
1781            public boolean isEntityDeepChanged(Identifiable entity) {
1782                    return dirtyCheckContext.isEntityDeepChanged(entity);
1783            }
1784        
1785        /**
1786         *  Discard changes of entity from last version received from the server
1787         *
1788         *  @param entity entity to restore
1789         */ 
1790        public void resetEntity(Identifiable entity) {
1791            if (entity == null)
1792                    throw new IllegalArgumentException("Entity cannot be null");
1793            
1794            EntityManager em = PersistenceManager.getEntityManager(entity);
1795            if (em == null)
1796                    return;
1797            
1798            if (em != this)
1799                    throw new IllegalArgumentException("Cannot reset an entity attached to another entity manager " + entity);
1800            
1801            Set<Object> cache = new HashSet<Object>();
1802            resetEntity(entity, cache);
1803        }
1804    
1805        private void resetEntity(Identifiable entity, Set<Object> cache) {
1806            try {
1807                MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, null);
1808                // Disable dirty check during reset of entity
1809                mergeContext.setMerging(true);
1810                dirtyCheckContext.resetEntity(mergeContext, entity, entity, cache);
1811            }
1812            finally {
1813                MergeContext.destroy(this);
1814            }
1815        }
1816    
1817        /**
1818         *  Discard changes of all cached entities from last version received from the server
1819         * 
1820         *  @param cache reset cache
1821         */ 
1822        public void resetAllEntities() {
1823            try {
1824                Set<Object> cache = new HashSet<Object>();
1825                
1826                MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, null);
1827                // Disable dirty check during reset of entity
1828                mergeContext.setMerging(true);
1829                dirtyCheckContext.resetAllEntities(mergeContext, cache);
1830            }
1831            finally {
1832                MergeContext.destroy(this);
1833            }
1834        }
1835        
1836        /**
1837         *  {@inheritdoc}
1838         */ 
1839        public void acceptConflict(Conflict conflict, boolean client) {
1840            boolean saveTracking = trackingContext.isEnabled();
1841            try {
1842                trackingContext.setEnabled(false);
1843                
1844                Object modifiedEntity = null;
1845                if (client) {
1846                    // Copy the local entity to save local changes
1847                    EntityManager entityManager = PersistenceManager.getEntityManager(conflict.getLocalEntity());
1848                    EntityManager tmp = entityManager.newTemporaryEntityManager();
1849                    modifiedEntity = tmp.mergeFromEntityManager(entityManager, conflict.getLocalEntity(), null, false);
1850                    tmp.clear();
1851                }
1852                else
1853                    modifiedEntity = conflict.getReceivedEntity();
1854                
1855                try {
1856                    MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, null);
1857                    
1858                    // Reset the local entity to its last stable state
1859                    resetEntity(conflict.getLocalEntity());
1860                    
1861                    if (client) {
1862                        // Merge with the incoming entity (to update version, id and all)
1863                        if (conflict.getReceivedEntity() != null)
1864                            mergeExternal(mergeContext, conflict.getReceivedEntity(), conflict.getLocalEntity(), null, null, null, null, false);
1865                    }
1866                    
1867                    // Finally reapply local changes on merged received result
1868                    resolveMergeConflicts(mergeContext, modifiedEntity, conflict.getLocalEntity(), client);
1869                }
1870                finally {
1871                    MergeContext.destroy(this);
1872                }
1873            }
1874            finally {
1875                trackingContext.setEnabled(saveTracking);
1876            }
1877        }
1878        
1879        
1880        private RemoteInitializer remoteInitializer = null;
1881        
1882        @Override
1883        public void setRemoteInitializer(RemoteInitializer remoteInitializer) {
1884            this.remoteInitializer = remoteInitializer;
1885        }
1886        
1887        /**
1888         *  {@inheritdoc}
1889         */
1890        public boolean initializeObject(ServerSession serverSession, Object object) {
1891            boolean initialize = false;
1892            if (remoteInitializer != null) {
1893                boolean saveTracking = trackingContext.isEnabled();
1894                try {
1895                    trackingContext.setEnabled(false);
1896                    initialize = remoteInitializer.initializeObject(serverSession, object);
1897                }
1898                finally {
1899                    trackingContext.setEnabled(saveTracking);
1900                }
1901            }
1902            return initialize;
1903        }
1904    //    
1905    //    /**
1906    //     *  {@inheritdoc}
1907    //     */
1908    //    public boolean validateObject(Object object, String property, Object value) {
1909    //        boolean validate = false;
1910    //        if (remoteValidator != null) {
1911    //            boolean saveTracking = trackingContext.isEnabled();
1912    //            try {
1913    //                trackingContext.setEnabled(false);
1914    //                validate = remoteValidator.validateObject(object, property, value);
1915    //            }
1916    //            finally {
1917    //                trackingContext.setEnabled(saveTracking);
1918    //            }
1919    //        }
1920    //        return validate;
1921    //    }
1922    
1923        /**
1924         *  @private 
1925         *  Interceptor for managed entity setters
1926         *
1927         *  @param entity entity to intercept
1928         *  @param propName property name
1929         *  @param oldValue old value
1930         *  @param newValue new value
1931         */ 
1932        public void setEntityProperty(Identifiable entity, String propName, Object oldValue, Object newValue) {
1933            if (newValue != oldValue) {
1934                if (oldValue != null) {
1935                    removeReference(oldValue, entity, propName, null);
1936                    dataManager.stopTracking(oldValue, entity);
1937                }
1938                
1939                if (newValue instanceof Identifiable || newValue instanceof List<?> || newValue instanceof Map<?, ?>) {
1940                    addReference(newValue, entity, propName, null);
1941                    dataManager.startTracking(newValue, entity);
1942                }
1943            }
1944            
1945            MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(entity));
1946            if (mergeContext == null)
1947                return;
1948            
1949            if (!mergeContext.isMerging() || mergeContext.isResolvingConflict())
1950                dirtyCheckContext.entityPropertyChangeHandler(entity, entity, propName, oldValue, newValue);
1951            
1952            addUpdates(entity);
1953        }
1954    
1955    
1956        /**
1957         *  @private 
1958         *  Interceptor for managed entity getters
1959         *
1960         *  @param entity entity to intercept
1961         *  @param propName property name
1962         *  @param value value
1963         * 
1964         *  @return value
1965         */ 
1966        public Object getEntityProperty(Identifiable entity, String propName, Object value) {
1967            if (value instanceof Identifiable || value instanceof List<?> || value instanceof Map<?, ?> || value instanceof ManagedPersistentAssociation)
1968                addResults(entity, propName);
1969            
1970            EntityDescriptor desc = dataManager.getEntityDescriptor(entity);
1971            if (desc != null && propName.equals(desc.getDirtyPropertyName()))
1972                return dirtyCheckContext.isEntityChanged(entity);
1973            
1974            return value;
1975        }
1976    
1977        
1978        public class DefaultTrackingHandler implements DataManager.TrackingHandler {
1979            
1980            /**
1981             *  @private 
1982             *  Property event handler to save changes on embedded objects
1983             *
1984             *  @param event collection event
1985             */ 
1986            public void entityPropertyChangeHandler(Object target, String property, Object oldValue, Object newValue) {
1987                MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(target));
1988                if ((mergeContext != null && mergeContext.getSourceEntityManager() == this) || !isActive())
1989                    return;
1990                
1991                if (newValue != oldValue) {
1992                    if (oldValue instanceof Identifiable || oldValue instanceof List<?> || oldValue instanceof Map<?, ?>) {
1993                        removeReference(oldValue, target, property, null);
1994                        dataManager.stopTracking(oldValue, target);
1995                    }
1996                    
1997                    if (newValue instanceof Identifiable || newValue instanceof List<?> || newValue instanceof Map<?, ?>) {
1998                        addReference(newValue, target, property, null);
1999                        dataManager.startTracking(newValue, target);
2000                    }
2001                }
2002                
2003                log.debug("property changed: %s %s", ObjectUtil.toString(target), property);
2004                
2005                if (mergeContext == null || !mergeContext.isMerging() || mergeContext.isResolvingConflict()) {
2006                    Object owner = target instanceof Identifiable ? null : getOwnerEntity(target);
2007                    if (owner == null)
2008                        dirtyCheckContext.entityPropertyChangeHandler(target, target, property, oldValue, newValue);
2009                    else if (owner instanceof Object[] && ((Object[])owner)[0] instanceof Identifiable)
2010                        dirtyCheckContext.entityPropertyChangeHandler(((Object[])owner)[0], target, property, oldValue, newValue);
2011                }
2012                
2013                // TODO: EntityManager embedded
2014        //        PropertyChangeEvent pce = new PropertyChangeEvent("entityEmbeddedChange", event.property, event.oldValue, event.newValue, event.source);
2015        //        dispatchEvent(pce);
2016            }
2017            
2018            /**
2019             *  @private 
2020             *  Collection event handler to save changes on collections
2021             *
2022             *  @param event collection event
2023             */ 
2024            public void collectionChangeHandler(ChangeKind kind, Object target, int location, Object[] items) {
2025                MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(target));
2026                if ((mergeContext != null && mergeContext.getSourceEntityManager() == this) || !isActive())
2027                    return;
2028                
2029                if (target instanceof Component)
2030                    return;
2031                
2032                if (kind == ChangeKind.ADD || kind == ChangeKind.REMOVE || kind == ChangeKind.REPLACE)
2033                    addUpdates(target);
2034            }
2035            
2036            /**
2037             *  @private 
2038             *  Collection event handler to save changes on managed collections
2039             *
2040             *  @param event collection event
2041             */ 
2042            public void entityCollectionChangeHandler(ChangeKind kind, Object target, int location, Object[] items) {
2043                MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(target));
2044                if ((mergeContext != null && mergeContext.getSourceEntityManager() == this) || !isActive())
2045                    return;
2046                
2047                int i = 0;
2048                
2049                Object[] parent = null;
2050                if (kind == ChangeKind.ADD && items != null && items.length > 0) {
2051                    parent = getOwnerEntity(target);
2052                    for (i = 0; i < items.length; i++) {
2053                        if (items[i] instanceof Identifiable) {
2054                            if (parent != null)
2055                                addReference((Identifiable)items[i], parent[0], (String)parent[1], null);
2056                            else
2057                                attachEntity((Identifiable)items[i]);
2058                            dataManager.startTracking((Identifiable)items[i], parent != null ? parent[0] : null);
2059                        }
2060                    }
2061                }
2062                else if (kind == ChangeKind.REMOVE && items != null && items.length > 0) {
2063                    parent = getOwnerEntity(target);
2064                    if (parent != null) {
2065                        for (i = 0; i < items.length; i++) {
2066                            if (items[i] instanceof Identifiable)
2067                                removeReference((Identifiable)items[i], parent[0], (String)parent[1], null);
2068                        }
2069                    }
2070                }
2071                else if (kind == ChangeKind.REPLACE && items != null && items.length > 0) {
2072                    parent = getOwnerEntity(target);
2073                    for (i = 0; i < items.length; i++) {
2074                        Object newValue = ((Object[])items[i])[1];
2075                        if (newValue instanceof Identifiable) {
2076                            if (parent != null)
2077                                addReference((Identifiable)newValue, parent[0], (String)parent[1], null);
2078                            else
2079                                attachEntity((Identifiable)newValue);
2080                            dataManager.startTracking((Identifiable)newValue, parent != null ? parent[0] : null);
2081                        }
2082                    }
2083                }
2084                
2085                if (!(kind == ChangeKind.ADD || kind == ChangeKind.REMOVE || kind == ChangeKind.REPLACE))
2086                    return;
2087                
2088                log.debug("collection changed: %s %s", kind, ObjectUtil.toString(target));
2089                
2090                if (mergeContext == null || !mergeContext.isMerging() || mergeContext.isResolvingConflict()) {
2091                    if (parent == null)
2092                        log.warn("Owner entity not found for collection %s, cannot process dirty checking", ObjectUtil.toString(target));
2093                    else
2094                        dirtyCheckContext.entityCollectionChangeHandler(parent[0], (String)parent[1], (Collection<?>)target, kind, location, items);
2095                }
2096                
2097                if (items != null && items.length > 0 && items[0] instanceof Identifiable)
2098                    addUpdates(target);
2099                else if (kind == ChangeKind.UPDATE && items != null && items.length > 0) {
2100                    PropertyChange pc = (PropertyChange)items[0];
2101                    if (pc.getObject() instanceof Identifiable)
2102                        addUpdates(target);
2103                }
2104            }
2105            
2106            /**
2107             *  @private 
2108             *  Map event handler to save changes on maps
2109             *
2110             *  @param event map event
2111             */ 
2112            public void mapChangeHandler(ChangeKind kind, Object target, int location, Object[] items) {
2113                MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(target));
2114                if ((mergeContext != null && mergeContext.getSourceEntityManager() == this) || !isActive())
2115                    return;
2116                
2117                if (target instanceof Component)
2118                    return;
2119                
2120                if (kind == ChangeKind.ADD || kind == ChangeKind.REMOVE || kind == ChangeKind.REPLACE)
2121                    addUpdates(target);
2122            }
2123            
2124            /**
2125             *  @private 
2126             *  Collection event handler to save changes on managed maps
2127             *
2128             *  @param event map event
2129             */ 
2130            public void entityMapChangeHandler(ChangeKind kind, Object target, int location, Object[] items) {
2131                MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(target));
2132                if ((mergeContext != null && mergeContext.getSourceEntityManager() == this) || !isActive())
2133                    return;
2134                
2135                Object[] parent = null;
2136                if (kind == ChangeKind.ADD && items != null && items.length > 0) {
2137                    parent = getOwnerEntity(target);
2138                    for (int i = 0; i < items.length; i++) {
2139                        if (items[i] instanceof Identifiable) {
2140                            if (parent != null)
2141                                addReference((Identifiable)items[i], parent[0], (String)parent[1], null);
2142                            else
2143                                attachEntity((Identifiable)items[i]);
2144                            dataManager.startTracking((Identifiable)items[i], parent != null ? parent[0] : null);
2145                        }
2146                        else if (items[i] instanceof Object[]) {
2147                            Object[] obj = (Object[])items[i];
2148                            if (obj[0] instanceof Identifiable) {
2149                                if (parent != null)
2150                                    addReference((Identifiable)obj[0], parent[0], (String)parent[1], null);
2151                                else
2152                                    attachEntity((Identifiable)obj[0]);
2153                                dataManager.startTracking((Identifiable)obj[0], parent != null ? parent[0] : null);
2154                            }
2155                            if (obj[1] instanceof Identifiable) {
2156                                if (parent != null)
2157                                    addReference((Identifiable)obj[1], parent[0], (String)parent[1], null);
2158                                else
2159                                    attachEntity((Identifiable)obj[1]);
2160                                dataManager.startTracking((Identifiable)obj[1], parent != null ? parent[0] : null);
2161                            }
2162                        }
2163                    }
2164                }
2165                else if (kind == ChangeKind.REMOVE && items != null && items.length > 0) {
2166                    parent = getOwnerEntity(target);
2167                    if (parent != null) {
2168                        for (int i = 0; i < items.length; i++) {
2169                            if (items[i] instanceof Identifiable) {
2170                                removeReference((Identifiable)items[i], parent[0], (String)parent[1], null);
2171                            }
2172                            else if (items[i] instanceof Object[]) {
2173                                Object[] obj = (Object[])items[i];
2174                                if (obj[0] instanceof Identifiable) {
2175                                    removeReference((Identifiable)obj[0], parent[0], (String)parent[1], null);
2176                                }
2177                                if (obj[1] instanceof Identifiable) {
2178                                    removeReference((Identifiable)obj[1], parent[0], (String)parent[1], null);
2179                                }
2180                            }
2181                        }
2182                    }
2183                }
2184                else if (kind == ChangeKind.REPLACE && items != null && items.length > 0) {
2185                    parent = getOwnerEntity(target);
2186                    for (int i = 0; i < items.length; i++) {
2187                        Object[] item = (Object[])items[i];
2188                        if (item[1] instanceof Identifiable) {
2189                            if (parent != null)
2190                                removeReference((Identifiable)item[1], parent[0], (String)parent[1], null);
2191                        }
2192                        if (item[2] instanceof Identifiable) {
2193                            if (parent != null)
2194                                addReference((Identifiable)item[2], parent[0], (String)parent[1], null);
2195                            else
2196                                attachEntity((Identifiable)item[2]);
2197                            dataManager.startTracking((Identifiable)item[2], parent != null ? parent[0] : null);
2198                        }
2199                    }
2200                }
2201                
2202                if (!(kind == ChangeKind.ADD || kind == ChangeKind.REMOVE || kind == ChangeKind.REPLACE))
2203                    return;
2204                
2205                log.debug("map changed: %s %s", kind, ObjectUtil.toString(target));
2206                
2207                if (mergeContext == null || !mergeContext.isMerging() || mergeContext.isResolvingConflict()) {
2208                    if (parent == null)
2209                        log.warn("Owner entity not found for collection %s, cannot process dirty checking", ObjectUtil.toString(target));
2210                    else
2211                        dirtyCheckContext.entityMapChangeHandler(parent[0], (String)parent[1], (Map<?, ?>)target, kind, items);
2212                }
2213                
2214                if (items != null && items.length > 0 && items[0] instanceof Object[] && ((Object[])items[0])[1] instanceof Identifiable) {
2215                    addUpdates(target);
2216                }
2217                else if (kind == ChangeKind.UPDATE && items != null && items.length > 0) {
2218                    if (((PropertyChange)items[0]).getObject() instanceof Identifiable)
2219                        addUpdates(target);
2220                }
2221            }
2222        }
2223        
2224        /**
2225         *  @private 
2226         *  Track updates on target object
2227         *
2228         *  @param object tracked object
2229         */ 
2230        private void addUpdates(Object object) {
2231            Expression ref = getReference(object, true, new HashSet<Object>());
2232            if (ref != null && expressionEvaluator != null) {
2233                Value value = expressionEvaluator.evaluate(ref);
2234                trackingContext.addUpdate(value.componentName, value.componentClassName, ref.getExpression(), value.value);
2235            }
2236        }
2237        
2238        /**
2239         *  @private 
2240         *  Track results on target object
2241         *
2242         *  @param object tracked object
2243         *  @param propName property name on tracked object
2244         */ 
2245        private void addResults(Object object, String propName) {
2246            Expression ref = getReference(object, true, new HashSet<Object>());
2247            if (ref != null && expressionEvaluator != null) {
2248                Value value = expressionEvaluator.getInstance(ref, object);
2249                trackingContext.addResult(value.componentName, value.componentClassName, 
2250                        ref.getExpression() != null ? ref.getExpression() + "." + propName : propName, value.instance);
2251            }
2252        }
2253        
2254        /**
2255         *  @private
2256         *  Handle data updates
2257         *
2258         *  @param sourceSessionId sessionId from which data updates come (null when from current session) 
2259         *  @param updates list of data updates
2260         */
2261        public void handleUpdates(MergeContext mergeContext, String sourceSessionId, List<Update> updates) {
2262            List<Object> merges = new ArrayList<Object>();
2263            List<Object> removals = new ArrayList<Object>();
2264            List<Object> persists = new ArrayList<Object>();
2265            
2266            for (Update update : updates) {
2267                if (update.getKind() == UpdateKind.PERSIST || update.getKind() == UpdateKind.UPDATE)
2268                    merges.add(update.getEntity());
2269                else if (update.getKind() == UpdateKind.REMOVE)
2270                    removals.add(update.getEntity());
2271                if (update.getKind() == UpdateKind.PERSIST)
2272                    persists.add(update.getEntity());
2273            }
2274            
2275            mergeContext.setExternalDataSessionId(sourceSessionId);
2276            internalMergeExternalData(mergeContext, merges, null, removals, persists);
2277            
2278            for (Update update : updates)
2279                update.setEntity(getCachedObject(update.getEntity(), update.getKind() != UpdateKind.REMOVE));
2280        }
2281        
2282            public void raiseUpdateEvents(Context context, List<EntityManager.Update> updates) {
2283                    List<String> refreshes = new ArrayList<String>();
2284                    
2285                    for (EntityManager.Update update : updates) {
2286                            Object entity = update.getEntity();
2287                            
2288                            if (entity != null) {
2289                                    String entityName = entity instanceof EntityRef ? getUnqualifiedClassName(((EntityRef)entity).getClassName()) : entity.getClass().getSimpleName();
2290                                    String eventType = update.getKind().eventName() + "." + entityName;
2291                                    context.getEventBus().raiseEvent(context, eventType, entity);
2292                                    
2293                                    if (UpdateKind.PERSIST.equals(update.getKind()) || UpdateKind.REMOVE.equals(update.getKind())) {
2294                                            if (!refreshes.contains(entityName))
2295                                                    refreshes.add(entityName);
2296                                    } 
2297                            }
2298                    }
2299                    
2300                    for (String refresh : refreshes)
2301                            context.getEventBus().raiseEvent(context, UpdateKind.REFRESH.eventName() + "." + refresh);
2302            }
2303        
2304            private static String getUnqualifiedClassName(String className) {
2305                    int idx = className.lastIndexOf(".");
2306                    return idx >= 0 ? className.substring(idx+1) : className;
2307            }
2308    
2309    
2310        @Override
2311        public void setRemoteValidator(RemoteValidator remoteValidator) {
2312        }
2313    
2314    
2315        @Override
2316        public boolean validateObject(Object object, String property, Object value) {
2317            return false;
2318        }
2319    }