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