/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * *** * * Community License: GPL 3.0 * * This file is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * This file is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * *** * * Available Commercial License: GraniteDS SLA 1.0 * * This is the appropriate option if you are creating proprietary * applications and you are not prepared to distribute and share the * source code of your application under the GPL v3 license. * * Please visit http://www.granitedataservices.com/license for more * details. */ package org.granite.client.tide.data.impl; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import org.granite.client.persistence.collection.PersistentCollection; import org.granite.client.tide.Context; import org.granite.client.tide.collection.CollectionLoader; import org.granite.client.tide.data.Conflict; import org.granite.client.tide.data.DataConflictListener; import org.granite.client.tide.data.DataMerger; import org.granite.client.tide.data.EntityManager; import org.granite.client.tide.data.PersistenceManager; import org.granite.client.tide.data.RemoteInitializer; import org.granite.client.tide.data.RemoteValidator; import org.granite.client.tide.data.Value; import org.granite.client.tide.data.impl.UIDWeakSet.Matcher; import org.granite.client.tide.data.impl.UIDWeakSet.Operation; import org.granite.client.tide.data.spi.DataManager; import org.granite.client.tide.data.spi.DataManager.ChangeKind; import org.granite.client.tide.data.spi.DataManager.TrackingHandler; import org.granite.client.tide.data.spi.DirtyCheckContext; import org.granite.client.tide.data.spi.EntityRef; import org.granite.client.tide.data.spi.MergeContext; import org.granite.client.tide.server.ServerSession; import org.granite.client.util.WeakIdentityHashMap; import org.granite.logging.Logger; import org.granite.util.TypeUtil; /** * @author William DRAI */ public class EntityManagerImpl implements EntityManager { private static final Logger log = Logger.getLogger(EntityManagerImpl.class); private String id; private boolean active = false; private DataManager dataManager = null; private TrackingHandler trackingHandler = new DefaultTrackingHandler(); private DirtyCheckContext dirtyCheckContext = null; private UIDWeakSet entitiesByUid = null; private WeakIdentityHashMap<Object, List<Object>> entityReferences = new WeakIdentityHashMap<Object, List<Object>>(); private DataMerger[] customMergers = null; public EntityManagerImpl(String id, DataManager dataManager) { this.id = id; this.active = true; this.dataManager = dataManager != null ? dataManager : new JavaBeanDataManager(); this.dataManager.setTrackingHandler(this.trackingHandler); this.entitiesByUid = new UIDWeakSet(this.dataManager); this.dirtyCheckContext = new DirtyCheckContextImpl(this.dataManager); } /** * Return the entity manager id * * @return the entity manager id */ public String getId() { return id; } /** * {@inheritDoc} */ public boolean isActive() { return active; } /** * Clear the current context * Destroys all components/context variables */ public void clear() { entitiesByUid.apply(new Operation() { @Override public void apply(Object o) { PersistenceManager.setEntityManager(o, null); } }); entitiesByUid.clear(); entityReferences.clear(); dirtyCheckContext.clear(false); dataManager.clear(); active = true; } /** * Clears entity cache */ public void clearCache() { // _mergeContext.clear(); } public DataManager getDataManager() { return dataManager; } public TrackingHandler getTrackingHandler() { return trackingHandler; } /** * Setter for the array of custom mergers * * @param customMergers array of mergers */ public void setCustomMergers(DataMerger[] customMergers) { if (customMergers != null && customMergers.length > 0) this.customMergers = customMergers; else this.customMergers = null; } private boolean uninitializeAllowed = true; @Override public void setUninitializeAllowed(boolean uninitializeAllowed) { this.uninitializeAllowed = uninitializeAllowed; } @Override public boolean isUninitializeAllowed() { return uninitializeAllowed; } private Propagation entityManagerPropagation = null; /** * Setter for the propagation manager * * @param propagation propagation function that will visit child entity managers */ public void setEntityManagerPropagation(Propagation propagation) { this.entityManagerPropagation = propagation; } /** * Setter for active flag * When EntityManager is not active, dirty checking is disabled * * @param active state */ public void setActive(boolean active) { this.active = active; } /** * Setter for dirty check context implementation * * @param dirtyCheckContext dirty check context implementation */ public void setDirtyCheckContext(DirtyCheckContext dirtyCheckContext) { if (dirtyCheckContext == null) throw new IllegalArgumentException("Dirty check context cannot be null"); this.dirtyCheckContext = dirtyCheckContext; } private static int tmpEntityManagerId = 1; /** * Create a new temporary entity manager */ public EntityManager newTemporaryEntityManager() { try { DataManager tmpDataManager = TypeUtil.newInstance(dataManager.getClass(), DataManager.class); return new EntityManagerImpl("$$TMP$$" + (tmpEntityManagerId++), tmpDataManager); } catch (Exception e) { throw new RuntimeException("Could not create temporaty entity manager", e); } } /** * Attach an entity to this context * * @param entity an entity */ public void attachEntity(Object entity) { attachEntity(entity, true); } /** * Attach an entity to this context * * @param entity an entity * @param putInCache put entity in cache */ public void attachEntity(Object entity, boolean putInCache) { EntityManager em = PersistenceManager.getEntityManager(entity); if (em != null && em != this && !em.isActive()) { throw new Error("The entity instance " + entity + " cannot be attached to two contexts (current: " + em.getId() + ", new: " + id + ")"); } PersistenceManager.setEntityManager(entity, this); if (putInCache) { if (entitiesByUid.put(entity) == null) dirtyCheckContext.addUnsaved(entity); } } /** * Detach an entity from this context * * @param entity an entity * @param removeFromCache remove entity from cache * @param forceRemove remove even if persistent */ public void detachEntity(Object entity, boolean removeFromCache, boolean forceRemove) { if (!forceRemove) { if (dataManager.hasVersionProperty(entity) && dataManager.getVersion(entity) != null) return; } dirtyCheckContext.markNotDirty(entity, entity); PersistenceManager.setEntityManager(entity, null); if (removeFromCache) entitiesByUid.remove(dataManager.getCacheKey(entity)); } public void attach(Object object) { internalAttach(object, new IdentityHashMap<Object, Object>()); } private void internalAttach(Object object, IdentityHashMap<Object, Object> cache) { if (object == null || isSimple(object)) return; if (cache.containsKey(object)) return; cache.put(object, object); if (isEntity(object)) attachEntity(object); for (Map.Entry<String, Object> me : dataManager.getPropertyValues(object, false, true).entrySet()) { Object val = me.getValue(); if (val != null && !dataManager.isInitialized(val)) continue; if (val instanceof Collection<?>) { for (Object o : ((Collection<?>)val)) internalAttach(o, cache); } else if (val instanceof Map<?, ?>) { for (Map.Entry<?, ?> e : ((Map<?, ?>)val).entrySet()) { internalAttach(e.getKey(), cache); internalAttach(e.getValue(), cache); } } else if (!isSimple(val)) internalAttach(val, cache); } } public static boolean isSimple(Object object) { return ObjectUtil.isSimple(object) || object instanceof Enum || object instanceof byte[] || object instanceof Value; } /** * {@inheritDoc} */ public boolean isPersisted(Object entity) { if (dataManager.hasVersionProperty(entity) && dataManager.getVersion(entity) != null) return true; return false; } private boolean isInitialized(Object entity) { return dataManager.isInitialized(entity); } private boolean isEntity(Object entity) { return dataManager.isEntity(entity); } /** * Internal implementation of object detach * * @param object object * @param cache internal cache to avoid graph loops * @param forceRemove force removal even if persisted */ public void detach(Object object, IdentityHashMap<Object, Object> cache, boolean forceRemove) { if (object == null || ObjectUtil.isSimple(object)) return; if (!dataManager.isInitialized(object)) return; if (cache.containsKey(object)) return; cache.put(object, object); Map<String, Object> values = dataManager.getPropertyValues(object, true, true, false); if (isEntity(object) && entityReferences.containsKey(object)) { detachEntity(object, true, forceRemove); for (Entry<String, Object> me : values.entrySet()) removeReference(me.getValue(), object, me.getKey()); } } /** * Retrieve an entity in the cache from its uid * * @param object an entity * @param nullIfAbsent return null if entity not cached in context * * @return cached object with the same uid as the specified object */ public Object getCachedObject(Object object, boolean nullIfAbsent) { Object entity = null; if (isEntity(object)) { entity = entitiesByUid.get(dataManager.getCacheKey(object)); } else if (object instanceof EntityRef) { entity = entitiesByUid.get(((EntityRef)object).getClassName() + ":" + ((EntityRef)object).getUid()); } else if (object instanceof String) { entity = entitiesByUid.get((String)object); } if (entity != null) return entity; if (nullIfAbsent) return null; return object; } /** * Retrieve the owner entity of the provided object (collection/map/entity) * * @param object an entity * @return array containing owner entity and property name */ public Object[] getOwnerEntity(Object object) { List<Object> refs = entityReferences.get(object); if (refs == null) return null; for (int i = 0; i < refs.size(); i++) { if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0] instanceof String) return new Object[] { entitiesByUid.get((String)((Object[])refs.get(i))[0]), ((Object[])refs.get(i))[1] }; } return null; } /** * Retrieve the owner entity of the provided object (collection/map/entity) * * @param object an entity * @return list of arrays containing owner and property name */ public List<Object[]> getOwnerEntities(Object object) { List<Object> refs = entityReferences.get(object); if (refs == null) return null; List<Object[]> owners = new ArrayList<Object[]>(); for (int i = 0; i < refs.size(); i++) { if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0] instanceof String) { Object owner = entitiesByUid.get((String)((Object[])refs.get(i))[0]); if (owner != null) // May have been garbage collected owners.add(new Object[] { owner, ((Object[])refs.get(i))[1] }); } } return owners; } /** * Init references array for an object * * @param obj an entity * @return list of current references */ private List<Object> initRefs(Object obj) { List<Object> refs = entityReferences.get(obj); if (refs == null) { refs = new ArrayList<Object>(); entityReferences.put(obj, refs); } return refs; } /** * Register a reference to the provided object with either a parent or res * * @param obj an entity * @param parent the parent entity * @param propName name of the parent entity property that references the entity */ public void addReference(Object obj, Object parent, String propName) { if (isEntity(obj)) attachEntity(obj); dataManager.startTracking(obj, parent); List<Object> refs = entityReferences.get(obj); boolean found = false; if (isEntity(parent)) { String ref = dataManager.getCacheKey(parent); if (refs == null) refs = initRefs(obj); else { for (int i = 0; i < refs.size(); i++) { if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0].equals(ref)) { found = true; break; } } } if (!found) refs.add(new Object[] { ref, propName }); } else if (parent != null) { if (refs == null) refs = initRefs(obj); else { for (int i = 0; i < refs.size(); i++) { if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0].equals(parent)) { found = true; break; } } } if (!found) refs.add(new Object[] { parent, propName }); } } /** * Remove a reference on the provided object * * @param obj an entity * @param parent the parent entity to dereference * @param propName name of the parent entity property that references the entity * @return true if actually removed */ public boolean removeReference(Object obj, Object parent, String propName) { List<Object> refs = entityReferences.get(obj); if (refs == null) return true; int idx = -1; if (isEntity(parent)) { for (int i = 0; i < refs.size(); i++) { if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0].equals(dataManager.getCacheKey(parent))) { idx = i; break; } } } else if (parent != null) { for (int i = 0; i < refs.size(); i++) { if (refs.get(i) instanceof Object[] && ((Object[])refs.get(i))[0].equals(parent)) { idx = i; break; } } } if (idx >= 0) refs.remove(idx); boolean removed = false; if (refs.size() == 0) { entityReferences.remove(obj); removed = true; if (isEntity(obj)) detachEntity(obj, true, false); dataManager.stopTracking(obj, parent); } if (obj instanceof PersistentCollection && !((PersistentCollection<?>)obj).wasInitialized()) return removed; if (obj instanceof Iterable<?>) { for (Object elt : (Iterable<?>)obj) removeReference(elt, parent, propName); } else if (obj != null && obj.getClass().isArray()) { for (int i = 0; i < Array.getLength(obj); i++) removeReference(Array.get(obj, i), parent, propName); } else if (obj instanceof Map<?, ?>) { for (Entry<?, ?> me : ((Map<?, ?>)obj).entrySet()) { removeReference(me.getKey(), parent, propName); removeReference(me.getValue(), parent, propName); } } return removed; } public MergeContext initMerge(ServerSession serverSession) { if (serverSession != null) { DataMerger[] customMergers = serverSession.getContext().allByType(DataMerger.class); setCustomMergers(customMergers); } return new MergeContext(this, dirtyCheckContext, serverSession); } /** * Merge an object coming from the server in the context * * * @param mergeContext current merge context * @param obj external object * @param previous previously existing object in the context (null if no existing object) * @param parent parent object for collections * @param propertyName property name of the current object in the parent object * @param forceUpdate force update of property (used for externalized properties) * * @return merged object (should === previous when previous not null) */ @SuppressWarnings("unchecked") public Object mergeExternal(final MergeContext mergeContext, Object obj, Object previous, Object parent, String propertyName, boolean forceUpdate) { mergeContext.initMerge(); boolean saveMergeUpdate = mergeContext.isMergeUpdate(); boolean saveMerging = mergeContext.isMerging(); try { mergeContext.setMerging(true); int stackSize = mergeContext.getMergeStackSize(); boolean addRef = false; boolean fromCache = false; Object prev = mergeContext.getFromCache(obj); Object next = obj; if (prev != null) { next = prev; fromCache = true; } else { // Clear change tracking dataManager.stopTracking(previous, parent); if (obj == null) { next = null; } else if (((obj instanceof PersistentCollection && !((PersistentCollection<?>)obj).wasInitialized()) || (obj instanceof PersistentCollection && !(previous instanceof PersistentCollection))) && isEntity(parent) && propertyName != null) { next = mergePersistentCollection(mergeContext, (PersistentCollection<?>)obj, previous, parent, propertyName); addRef = true; } else if (obj instanceof List<?>) { next = mergeList(mergeContext, (List<Object>)obj, previous, parent, propertyName); addRef = true; } else if (obj instanceof Set<?>) { next = mergeSet(mergeContext, (Set<Object>)obj, previous, parent, propertyName); addRef = true; } else if (obj instanceof Map<?, ?>) { next = mergeMap(mergeContext, (Map<Object, Object>)obj, previous, parent, propertyName); addRef = true; } else if (obj.getClass().isArray()) { next = mergeArray(mergeContext, obj, previous, parent, propertyName); addRef = true; } else if (isEntity(obj)) { next = mergeEntity(mergeContext, obj, previous, parent, propertyName); addRef = true; } else { boolean merged = false; if (customMergers != null) { for (DataMerger merger : customMergers) { if (merger.accepts(obj)) { next = merger.merge(mergeContext, obj, previous, parent, propertyName); // Keep notified of collection updates to notify the server at next remote call dataManager.startTracking(previous, parent); merged = true; addRef = true; } } } if (!merged && !ObjectUtil.isSimple(obj) && !(obj instanceof Enum || obj instanceof Value || obj instanceof byte[])) { next = mergeEntity(mergeContext, obj, previous, parent, propertyName); addRef = true; } } } if (next != null && !fromCache && addRef && (prev == null && parent != null)) { // Store reference from current object to its parent entity or root component expression // If it comes from the cache, we are probably in a circular graph addReference(next, parent, propertyName); } mergeContext.setMergeUpdate(saveMergeUpdate); if (entityManagerPropagation != null && (mergeContext.isMergeUpdate() || forceUpdate) && !fromCache && isEntity(obj)) { // Propagate to existing conversation contexts where the entity is present entityManagerPropagation.propagate(obj, new Function() { public void execute(EntityManager entityManager, Object entity) { if (entityManager == mergeContext.getSourceEntityManager()) return; if (entityManager.getCachedObject(entity, true) != null) entityManager.mergeFromEntityManager(entityManager, mergeContext.getServerSession(), entity, mergeContext.getExternalDataSessionId(), mergeContext.isUninitializing()); } }); } if (mergeContext.getMergeStackSize() > stackSize) mergeContext.popMerge(); return next; } catch (Exception e) { throw new RuntimeException("Merge error", e); } finally { mergeContext.setMerging(saveMerging); } } /** * Merge an entity coming from the server in the context * * @param mergeContext current merge context * @param obj external entity * @param previous previously existing object in the context (null if no existing object) * @param parent parent object for collections * @param propertyName propertyName from the owner object * * @return merged entity (=== previous when previous not null) */ private Object mergeEntity(MergeContext mergeContext, final Object obj, Object previous, Object parent, String propertyName) { if (obj != null || previous != null) log.debug("mergeEntity: %s previous %s%s", ObjectUtil.toString(obj), ObjectUtil.toString(previous), obj == previous ? " (same)" : ""); Object dest = obj; Object p = null; if (!isInitialized(obj)) { // If entity is uninitialized, try to lookup the cached instance by its class name and id (only works with Hibernate proxies) if (dataManager.hasIdProperty(obj)) { p = entitiesByUid.find(new Matcher() { public boolean match(Object o) { return o.getClass().getName().equals(obj.getClass().getName()) && ObjectUtil.objectEquals(dataManager, dataManager.getId(obj), dataManager.getId(o)); } }); if (p != null) { previous = p; dest = previous; } } } else if (dataManager.isEntity(obj)) { p = entitiesByUid.get(dataManager.getCacheKey(obj)); if (p != null) { // Trying to merge an entity that is already cached with itself: stop now, this is not necessary to go deeper in the object graph // it should be already instrumented and tracked if (obj == p) return obj; previous = p; dest = previous; } } if (dest != previous && previous != null && (ObjectUtil.objectEquals(dataManager, previous, obj) || !isEntity(previous))) // GDS-649 Case of embedded objects dest = previous; if (dest == obj && p == null && obj != null && mergeContext.getSourceEntityManager() != null) { // When merging from another entity manager, ensure we create a new copy of the entity // An instance can exist in only one entity manager at a time try { dest = TypeUtil.newInstance(obj.getClass(), Object.class); dataManager.copyUid(dest, obj); } catch (Exception e) { throw new RuntimeException("Could not create class " + obj.getClass(), e); } } if (!isInitialized(obj) && ObjectUtil.objectEquals(dataManager, previous, obj)) { // Don't overwrite existing entity with an uninitialized proxy when optimistic locking is defined log.debug("ignored received uninitialized proxy"); // Don't mark the object not dirty as we only received a proxy // dirtyCheckContext.markNotDirty(previous, null); return previous; } if (!isInitialized(dest)) log.debug("initialize lazy entity: %s", dest.toString()); if (dest != null && isEntity(dest) && dest == obj) { log.debug("received entity %s used as destination (ctx: %s)", obj.toString(), this.id); } boolean fromCache = (p != null && dest == p); if (!fromCache && isEntity(dest)) entitiesByUid.put(dest); mergeContext.pushMerge(obj, dest); boolean tracking = false; if (mergeContext.isResolvingConflict()) { dataManager.startTracking(dest, parent); tracking = true; } boolean ignore = false; if (isEntity(dest)) { // If we are in an uninitialing temporary entity manager, try to reproxy associations when possible if (mergeContext.isUninitializing() && isEntity(parent) && propertyName != null) { if (dataManager.hasVersionProperty(dest) && dataManager.getVersion(obj) != null && dataManager.isLazyProperty(parent, propertyName)) { if (dataManager.defineProxy(dest, obj)) // Only if entity can be proxied (has a detachedState) return dest; } } // Associate entity with the current context attachEntity(dest, false); if (previous != null && dest == previous) { // Check version for optimistic locking if (dataManager.hasVersionProperty(dest) && !mergeContext.isResolvingConflict()) { Number newVersion = (Number)dataManager.getVersion(obj); Number oldVersion = (Number)dataManager.getVersion(dest); if ((newVersion != null && oldVersion != null && newVersion.longValue() < oldVersion.longValue() || (newVersion == null && oldVersion != null))) { log.warn("ignored merge of older version of %s (current: %d, received: %d)", dest.toString(), oldVersion, newVersion); ignore = true; } else if ((newVersion != null && oldVersion != null && newVersion.longValue() > oldVersion.longValue()) || (newVersion != null && oldVersion == null)) { // Handle changes when version number is increased mergeContext.markVersionChanged(dest); boolean entityChanged = dirtyCheckContext.isEntityChanged(dest); if (mergeContext.getExternalDataSessionId() != null && entityChanged && dirtyCheckContext.checkAndMarkNotDirty(mergeContext, dest, obj, null)) { // Conflict between externally received data and local modifications log.warn("conflict with external data detected on %s (current: %d, received: %d)", dest.toString(), oldVersion, newVersion); // Incoming data is different from local data Map<String, Object> save = dirtyCheckContext.getSavedProperties(dest); List<String> properties = new ArrayList<String>(save.keySet()); properties.remove(dataManager.getVersionPropertyName(dest)); Collections.sort(properties); mergeContext.addConflict(dest, obj, properties); ignore = true; } else mergeContext.setMergeUpdate(true); } else { // Data has been changed locally and not persisted, don't overwrite when version number is unchanged if (dirtyCheckContext.isEntityChanged(dest)) mergeContext.setMergeUpdate(false); else mergeContext.setMergeUpdate(true); } } else if (!mergeContext.isResolvingConflict()) mergeContext.markVersionChanged(dest); } else mergeContext.markVersionChanged(dest); if (!ignore) defaultMerge(mergeContext, obj, dest, parent, propertyName); } else defaultMerge(mergeContext, obj, dest, parent, propertyName); if (dest != null && !ignore && !mergeContext.isSkipDirtyCheck() && !mergeContext.isResolvingConflict()) dirtyCheckContext.checkAndMarkNotDirty(mergeContext, dest, obj, isEntity(parent) && !isEntity(dest) ? parent : null); if (dest != null) log.debug("mergeEntity result: %s", dest.toString()); // Keep notified of collection updates to notify the server at next remote call if (!tracking) dataManager.startTracking(dest, parent); return dest; } private Object mergeArray(MergeContext mergeContext, Object array, Object previous, Object parent, String propertyName) { Object dest = mergeContext.getSourceEntityManager() == null ? array : Array.newInstance(array.getClass().getComponentType(), Array.getLength(array)); mergeContext.pushMerge(array, dest); for (int i = 0; i < Array.getLength(array); i++) { Object obj = Array.get(array, i); obj = mergeExternal(mergeContext, obj, null, propertyName != null ? parent : null, propertyName, false); if (mergeContext.isMergeUpdate()) Array.set(dest, i, obj); } return dest; } /** * Merge a collection coming from the server in the context * * @param mergeContext current merge context * @param coll external collection * @param previous previously existing collection in the context (can be null if no existing collection) * @param parent owner object for collections * @param propertyName property name in owner object * * @return merged collection (=== previous when previous not null) */ @SuppressWarnings("unchecked") private List<?> mergeList(MergeContext mergeContext, List<Object> coll, Object previous, Object parent, String propertyName) { log.debug("mergeList: %s previous %s", ObjectUtil.toString(coll), ObjectUtil.toString(previous)); if (mergeContext.isUninitializing() && isEntity(parent) && propertyName != null) { if (dataManager.hasVersionProperty(parent) && dataManager.getVersion(parent) != null && dataManager.isLazyProperty(parent, propertyName)) { mergeContext.pushMerge(coll, previous); if (previous instanceof PersistentCollection && ((PersistentCollection<List<?>>)previous).wasInitialized()) { log.debug("uninitialize lazy collection %s", ObjectUtil.toString(previous)); ((PersistentCollection<List<?>>)previous).uninitialize(); } return (List<?>)previous; } } if (previous != null && previous instanceof PersistentCollection && !((PersistentCollection<List<?>>)previous).wasInitialized()) { log.debug("initialize lazy collection %s", ObjectUtil.toString(previous)); mergeContext.pushMerge(coll, previous); ((PersistentCollection<List<?>>)previous).initializing(); List<Object> added = new ArrayList<Object>(coll.size()); for (int i = 0; i < coll.size(); i++) { Object obj = coll.get(i); obj = mergeExternal(mergeContext, obj, null, propertyName != null ? parent : null, propertyName, false); added.add(obj); } ((PersistentCollection<List<?>>)previous).initialize(added, null); // Keep notified of collection updates to notify the server at next remote call dataManager.startTracking(previous, parent); return (List<?>)previous; } boolean tracking = false; List<?> nextList = null; List<Object> list = null; if (previous != null && previous instanceof List<?>) list = (List<Object>)previous; else if (mergeContext.getSourceEntityManager() != null) { try { list = coll.getClass().newInstance(); } catch (Exception e) { throw new RuntimeException("Could not create class " + coll.getClass()); } } else list = coll; mergeContext.pushMerge(coll, list); List<Object> prevColl = list != coll ? list : null; List<Object> destColl = prevColl; if (prevColl != null && mergeContext.isMergeUpdate()) { // Enable tracking before modifying collection when resolving a conflict // so the dirty checking can save changes if (mergeContext.isResolvingConflict()) { dataManager.startTracking(prevColl, parent); tracking = true; } for (int i = 0; i < destColl.size(); i++) { Object obj = destColl.get(i); boolean found = false; for (int j = 0; j < coll.size(); j++) { Object next = coll.get(j); if (ObjectUtil.objectEquals(dataManager, next, obj)) { found = true; break; } } if (!found) { destColl.remove(i); i--; } } } for (int i = 0; i < coll.size(); i++) { Object obj = coll.get(i); if (destColl != null) { boolean found = false; for (int j = i; j < destColl.size(); j++) { Object prev = destColl.get(j); if (i < destColl.size() && ObjectUtil.objectEquals(dataManager, prev, obj)) { obj = mergeExternal(mergeContext, obj, prev, propertyName != null ? parent : null, propertyName, false); if (j != i) { destColl.remove(j); if (i < destColl.size()) destColl.add(i, obj); else destColl.add(obj); if (i > j) j--; } else if (obj != prev) destColl.set(i, obj); found = true; } } if (!found) { obj = mergeExternal(mergeContext, obj, null, propertyName != null ? parent : null, propertyName, false); if (mergeContext.isMergeUpdate()) { if (i < prevColl.size()) destColl.add(i, obj); else destColl.add(obj); } } } else { Object prev = obj; obj = mergeExternal(mergeContext, obj, null, propertyName != null ? parent : null, propertyName, false); if (obj != prev) coll.set(i, obj); } } if (destColl != null && mergeContext.isMergeUpdate()) { if (!mergeContext.isResolvingConflict() && !mergeContext.isSkipDirtyCheck()) dirtyCheckContext.markNotDirty(previous, parent); nextList = prevColl; } else if (prevColl instanceof PersistentCollection && !mergeContext.isMergeUpdate()) { nextList = prevColl; } else nextList = coll; // Wrap/instrument persistent collections if (isEntity(parent) && propertyName != null && nextList instanceof PersistentCollection && !(((PersistentCollection<?>)nextList).getLoader() instanceof CollectionLoader)) { log.debug("instrument persistent collection from %s", ObjectUtil.toString(nextList)); ((PersistentCollection<List<?>>)nextList).setLoader(new CollectionLoader<List<?>>(mergeContext.getServerSession(), parent, propertyName)); } else log.debug("mergeList result: %s", ObjectUtil.toString(nextList)); mergeContext.pushMerge(coll, nextList, false); if (!tracking) dataManager.startTracking(nextList, parent); return nextList; } /** * Merge a collection coming from the server in the context * * @param mergeContext current merge context * @param coll external collection * @param previous previously existing collection in the context (can be null if no existing collection) * @param parent owner object for collections * @param propertyName property name in owner object * * @return merged collection (=== previous when previous not null) */ @SuppressWarnings("unchecked") private Set<?> mergeSet(MergeContext mergeContext, Set<Object> coll, Object previous, Object parent, String propertyName) { log.debug("mergeSet: %s previous %s", ObjectUtil.toString(coll), ObjectUtil.toString(previous)); if (mergeContext.isUninitializing() && isEntity(parent) && propertyName != null) { if (dataManager.hasVersionProperty(parent) && dataManager.getVersion(parent) != null && dataManager.isLazyProperty(parent, propertyName)) { mergeContext.pushMerge(coll, previous); if (previous instanceof PersistentCollection && ((PersistentCollection<Set<?>>)previous).wasInitialized()) { log.debug("uninitialize lazy collection %s", ObjectUtil.toString(previous)); ((PersistentCollection<Set<?>>)previous).uninitialize(); } return (Set<?>)previous; } } if (previous != null && previous instanceof PersistentCollection && !((PersistentCollection<Set<?>>)previous).wasInitialized()) { log.debug("initialize lazy collection %s", ObjectUtil.toString(previous)); mergeContext.pushMerge(coll, previous); ((PersistentCollection<Set<?>>)previous).initializing(); final Set<Object> added = new HashSet<Object>(coll.size()); for (Iterator<Object> icoll = coll.iterator(); icoll.hasNext(); ) { Object obj = icoll.next(); obj = mergeExternal(mergeContext, obj, null, propertyName != null ? parent : null, propertyName, false); added.add(obj); } ((PersistentCollection<Set<?>>)previous).initialize(added, null); // Keep notified of collection updates to notify the server at next remote call dataManager.startTracking(previous, parent); return (Set<?>)previous; } boolean tracking = false; Set<?> nextSet = null; Set<Object> set = null; if (previous != null && previous instanceof Set<?>) set = (Set<Object>)previous; else if (mergeContext.getSourceEntityManager() != null) { try { if (coll instanceof SortedSet<?>) { try { set = coll.getClass().getConstructor(Comparator.class).newInstance(((SortedSet<?>)coll).comparator()); } catch (NoSuchMethodException nsme) { } } if (set == null) set = coll.getClass().newInstance(); } catch (Exception e) { throw new RuntimeException("Could not create class " + coll.getClass()); } } else set = coll; mergeContext.pushMerge(coll, set); Set<Object> prevColl = set != coll ? set : null; Set<Object> destColl = prevColl; if (prevColl != null && mergeContext.isMergeUpdate()) { // Enable tracking before modifying collection when resolving a conflict // so the dirty checking can save changes if (mergeContext.isResolvingConflict()) { dataManager.startTracking(prevColl, parent); tracking = true; } for (Iterator<Object> ic = destColl.iterator(); ic.hasNext(); ) { Object obj = ic.next(); boolean found = false; for (Iterator<Object> jc = coll.iterator(); jc.hasNext(); ) { Object next = jc.next(); if (ObjectUtil.objectEquals(dataManager, next, obj)) { found = true; break; } } if (!found) ic.remove(); } } Set<Object> changed = new HashSet<Object>(); for (Iterator<Object> ic = coll.iterator(); ic.hasNext(); ) { Object obj = ic.next(); if (destColl != null) { boolean found = false; for (Iterator<Object> jc = destColl.iterator(); jc.hasNext(); ) { Object prev = jc.next(); if (ObjectUtil.objectEquals(dataManager, prev, obj)) { obj = mergeExternal(mergeContext, obj, prev, propertyName != null ? parent : null, propertyName, false); if (obj != prev) { ic.remove(); changed.add(obj); } found = true; } } if (!found) { obj = mergeExternal(mergeContext, obj, null, propertyName != null ? parent : null, propertyName, false); if (mergeContext.isMergeUpdate()) destColl.add(obj); } } else { Object prev = obj; obj = mergeExternal(mergeContext, obj, null, propertyName != null ? parent : null, propertyName, false); if (obj != prev) { ic.remove(); changed.add(obj); } } } if (destColl != null) destColl.addAll(changed); else coll.addAll(changed); if (destColl != null && mergeContext.isMergeUpdate()) { if (!mergeContext.isResolvingConflict() && !mergeContext.isSkipDirtyCheck()) dirtyCheckContext.markNotDirty(previous, parent); nextSet = prevColl; } else if (prevColl instanceof PersistentCollection && !mergeContext.isMergeUpdate()) { nextSet = prevColl; } else nextSet = coll; // Wrap/instrument persistent collections if (isEntity(parent) && propertyName != null && nextSet instanceof PersistentCollection && !(((PersistentCollection<Set<?>>)nextSet).getLoader() instanceof CollectionLoader)) { log.debug("instrument persistent collection from %s", ObjectUtil.toString(nextSet)); ((PersistentCollection<Set<?>>)nextSet).setLoader(new CollectionLoader<Set<?>>(mergeContext.getServerSession(), parent, propertyName)); } else log.debug("mergeSet result: %s", ObjectUtil.toString(nextSet)); mergeContext.pushMerge(coll, nextSet, false); if (!tracking) dataManager.startTracking(nextSet, parent); return nextSet; } /** * Merge a map coming from the server in the context * * @param mergeContext current merge context * @param map external map * @param previous previously existing map in the context (null if no existing map) * @param parent owner object for the map if applicable * @param propertyName property name from the owner * * @return merged map (=== previous when previous not null) */ @SuppressWarnings("unchecked") private Map<?, ?> mergeMap(MergeContext mergeContext, Map<Object, Object> map, Object previous, Object parent, String propertyName) { log.debug("mergeMap: %s previous %s", ObjectUtil.toString(map), ObjectUtil.toString(previous)); if (mergeContext.isUninitializing() && isEntity(parent) && propertyName != null) { if (dataManager.hasVersionProperty(parent) && dataManager.getVersion(parent) != null && dataManager.isLazyProperty(parent, propertyName)) { mergeContext.pushMerge(map, previous); if (previous instanceof PersistentCollection && ((PersistentCollection<Map<?, ?>>)previous).wasInitialized()) { log.debug("uninitialize lazy map %s", ObjectUtil.toString(previous)); ((PersistentCollection<Map<?, ?>>)previous).uninitialize(); } return (Map<?, ?>)previous; } } if (previous != null && previous instanceof PersistentCollection && !((PersistentCollection<Map<?, ?>>)previous).wasInitialized()) { log.debug("initialize lazy map %s", ObjectUtil.toString(previous)); mergeContext.pushMerge(map, previous); ((PersistentCollection<Map<?, ?>>)previous).initializing(); Map<Object, Object> added = new HashMap<Object, Object>(); for (Entry<?, ?> me : map.entrySet()) { Object key = mergeExternal(mergeContext, me.getKey(), null, propertyName != null ? parent: null, propertyName, false); Object value = mergeExternal(mergeContext, me.getValue(), null, propertyName != null ? parent : null, propertyName, false); added.put(key, value); } ((PersistentCollection<Map<?, ?>>)previous).initialize(added, null); // Keep notified of collection updates to notify the server at next remote call dataManager.startTracking(previous, parent); return (Map<?, ?>)previous; } boolean tracking = false; Map<Object, Object> nextMap = null; Map<Object, Object> m = null; if (previous != null && previous instanceof Map<?, ?>) m = (Map<Object, Object>)previous; else if (mergeContext.getSourceEntityManager() != null) { try { if (map instanceof SortedMap<?, ?>) { try { m = map.getClass().getConstructor(Comparator.class).newInstance(((SortedMap<?, ?>)map).comparator()); } catch (NoSuchMethodException nsme) { } } if (m == null) m = TypeUtil.newInstance(map.getClass(), Map.class); } catch (Exception e) { throw new RuntimeException("Could not create class " + map.getClass()); } } else m = map; mergeContext.pushMerge(map, m); Map<Object, Object> prevMap = m != map ? m : null; if (prevMap != null) { if (mergeContext.isResolvingConflict()) { dataManager.startTracking(prevMap, parent); tracking = true; } if (map != prevMap) { for (Entry<?, ?> me : map.entrySet()) { Object newKey = mergeExternal(mergeContext, me.getKey(), null, parent, propertyName, false); Object prevValue = prevMap.get(newKey); Object value = mergeExternal(mergeContext, me.getValue(), prevValue, parent, propertyName, false); if (mergeContext.isMergeUpdate() || prevMap.containsKey(newKey)) prevMap.put(newKey, value); } if (mergeContext.isMergeUpdate()) { Iterator<Object> imap = prevMap.keySet().iterator(); while (imap.hasNext()) { Object key = imap.next(); boolean found = false; for (Object k : map.keySet()) { if (ObjectUtil.objectEquals(dataManager, k, key)) { found = true; break; } } if (!found) imap.remove(); } } } if (mergeContext.isMergeUpdate() && !mergeContext.isResolvingConflict() && !mergeContext.isSkipDirtyCheck()) dirtyCheckContext.markNotDirty(previous, parent); nextMap = prevMap; } else { List<Object[]> addedToMap = new ArrayList<Object[]>(); for (Entry<?, ?> me : map.entrySet()) { Object value = mergeExternal(mergeContext, me.getValue(), null, parent, propertyName, false); Object key = mergeExternal(mergeContext, me.getKey(), null, parent, propertyName, false); addedToMap.add(new Object[] { key, value }); } map.clear(); for (Object[] obj : addedToMap) map.put(obj[0], obj[1]); nextMap = map; } if (isEntity(parent) && propertyName != null && nextMap instanceof PersistentCollection && !(((PersistentCollection<Map<?, ?>>)nextMap).getLoader() instanceof CollectionLoader)) { log.debug("instrument persistent map from %s", ObjectUtil.toString(nextMap)); ((PersistentCollection<Map<?, ?>>)nextMap).setLoader(new CollectionLoader<Map<?, ?>>(mergeContext.getServerSession(), parent, propertyName)); } else log.debug("mergeMap result: %s", ObjectUtil.toString(nextMap)); mergeContext.pushMerge(map, nextMap, false); if (!tracking) dataManager.startTracking(nextMap, parent); return nextMap; } /** * Wraps a persistent collection to manage lazy initialization * * @param mergeContext current merge context * @param coll the collection to wrap * @param previous the previous existing collection * @param parent the owner object * @param propertyName owner property * * @return the wrapped persistent collection */ @SuppressWarnings("unchecked") protected Object mergePersistentCollection(MergeContext mergeContext, PersistentCollection<?> coll, Object previous, Object parent, String propertyName) { if (previous instanceof PersistentCollection) { mergeContext.pushMerge(coll, previous); if (((PersistentCollection<?>)previous).wasInitialized()) { if (mergeContext.isUninitializeAllowed() && mergeContext.hasVersionChanged(parent)) { log.debug("uninitialize lazy collection %s", ObjectUtil.toString(previous)); ((PersistentCollection<?>)previous).uninitialize(); } else log.debug("keep initialized collection %s", ObjectUtil.toString(previous)); } if (!(((PersistentCollection<?>)previous).getLoader() instanceof CollectionLoader)) { log.debug("instrument persistent collection from %s", ObjectUtil.toString(previous)); ((PersistentCollection<Object>)previous).setLoader(new CollectionLoader<Object>(mergeContext.getServerSession(), parent, propertyName)); } dataManager.startTracking(previous, parent); return previous; } PersistentCollection<?> pcoll = coll; if (previous instanceof PersistentCollection) pcoll = (PersistentCollection<?>)previous; if (coll.getLoader() instanceof CollectionLoader) pcoll = duplicatePersistentCollection(mergeContext, coll, parent, propertyName); else if (mergeContext.getSourceEntityManager() != null) pcoll = duplicatePersistentCollection(mergeContext, pcoll, parent, propertyName); mergeContext.pushMerge(coll, pcoll); if (pcoll.wasInitialized()) { if (pcoll instanceof List<?>) { List<Object> plist = (List<Object>)pcoll; for (int i = 0; i < plist.size(); i++) { Object obj = mergeExternal(mergeContext, plist.get(i), null, parent, propertyName, false); if (obj != plist.get(i)) plist.set(i, obj); } } else { Collection<Object> pset = (Collection<Object>)pcoll; List<Object> toAdd = new ArrayList<Object>(); for (Iterator<Object> iset = pset.iterator(); iset.hasNext(); ) { Object obj = iset.next(); Object merged = mergeExternal(mergeContext, obj, null, parent, propertyName, false); if (merged != obj) { iset.remove(); toAdd.add(merged); } } pset.addAll(toAdd); } dataManager.startTracking(pcoll, parent); } else if (isEntity(parent) && propertyName != null) dataManager.setLazyProperty(parent, propertyName); if (!(coll.getLoader() instanceof CollectionLoader)) { log.debug("instrument persistent collection from %s", ObjectUtil.toString(pcoll)); ((PersistentCollection<Object>)pcoll).setLoader(new CollectionLoader<Object>(mergeContext.getServerSession(), parent, propertyName)); } return pcoll; } private PersistentCollection<?> duplicatePersistentCollection(MergeContext mergeContext, Object coll, Object parent, String propertyName) { if (!(coll instanceof PersistentCollection)) throw new RuntimeException("Not a persistent collection/map " + ObjectUtil.toString(coll)); PersistentCollection<?> ccoll = ((PersistentCollection<?>)coll).clone(mergeContext.isUninitializing()); if (mergeContext.isUninitializing() && parent != null && propertyName != null) { if (dataManager.hasVersionProperty(parent) && dataManager.getVersion(parent) != null && dataManager.isLazyProperty(parent, propertyName)) ccoll.uninitialize(); } return ccoll; } /** * Merge an object coming from another entity manager (in general in the global context) in the local context * * @param sourceEntityManager source context of incoming data * @param serverSession current server session * @param obj external object * @param externalDataSessionId is merge from external data * @param uninitializing true to force folding of loaded lazy associations * * @return merged object */ public Object mergeFromEntityManager(EntityManager sourceEntityManager, ServerSession serverSession, Object obj, String externalDataSessionId, boolean uninitializing) { try { MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, serverSession); mergeContext.setSourceEntityManager(sourceEntityManager); mergeContext.setUninitializing(uninitializing); mergeContext.setExternalDataSessionId(externalDataSessionId); Object next = externalDataSessionId != null ? internalMergeExternalData(mergeContext, obj, null, null, null) // Force handling of external data : mergeExternal(mergeContext, obj, null, null, null, false); return next; } finally { MergeContext.destroy(this); } } /** * Merge an object coming from a remote location (in general from a service) in the local context * * @param obj external object * * @return merged object (should === previous when previous not null) */ public Object mergeExternalData(Object obj) { return mergeExternalData(null, obj, null, null, null, null); } public Object mergeExternalData(ServerSession serverSession, Object obj) { return mergeExternalData(serverSession, obj, null, null, null, null); } public Object mergeExternalData(Object obj, Object prev, String externalDataSessionId, List<Object> removals, List<Object> persists) { return mergeExternalData(null, obj, prev, externalDataSessionId, removals, persists); } /** * Merge an object coming from a remote location (in general from a service) in the local context * * @param serverSession server session * @param obj external object * @param prev existing local object to merge with * @param externalDataSessionId sessionId from which the data is coming (other user/server), null if local or current user session * @param removals list of entities to remove from the entity manager cache * @param persists list of newly persisted entities * * @return merged object (should === previous when previous not null) */ public Object mergeExternalData(ServerSession serverSession, Object obj, Object prev, String externalDataSessionId, List<Object> removals, List<Object> persists) { try { MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, serverSession); mergeContext.setExternalDataSessionId(externalDataSessionId); return internalMergeExternalData(mergeContext, obj, prev, removals, persists); } finally { MergeContext.destroy(this); } } /** * Merge an object coming from a remote location (in general from a service) in the local context * * @param mergeContext current merge context * @param obj external object * @param prev existing local object to merge with * @param removals array of entities to remove from the entity manager cache * @param persists list of newly persisted entities * * @return merged object (should === previous when previous not null) */ public Object internalMergeExternalData(MergeContext mergeContext, Object obj, Object prev, List<Object> removals, List<Object> persists) { Object next = mergeExternal(mergeContext, obj, prev, null, null, false); if (removals != null) handleRemovalsAndPersists(mergeContext, removals, persists); if (mergeContext.getExternalDataSessionId() != null) { handleMergeConflicts(mergeContext); clearCache(); } return next; } /** * Merge conversation entity manager context variables in global entity manager * Only applicable to conversation contexts * * @param entityManager conversation entity manager */ public void mergeInEntityManager(final EntityManager entityManager, final ServerSession serverSession) { final Set<Object> cache = new HashSet<Object>(); final EntityManager sourceEntityManager = this; entitiesByUid.apply(new UIDWeakSet.Operation() { public void apply(Object obj) { // Reset local dirty state, only server state can safely be merged in global context if (isEntity(obj)) resetEntity(obj, cache); entityManager.mergeFromEntityManager(sourceEntityManager, serverSession, obj, null, false); } }); } @Override public boolean isDirty() { return dataManager.isDirty(); } public boolean isDirtyEntity(Object entity) { return dirtyCheckContext.isEntityChanged(entity); } public boolean isDeepDirtyEntity(Object entity) { return dirtyCheckContext.isEntityDeepChanged(entity); } public boolean isSavedEntity(Object entity) { return dirtyCheckContext.getSavedProperties(entity) != null; } /** * Remove elements from cache and managed collections * * @param mergeContext current merge context * @param removals list of entity instances to remove from the entity manager cache * @param persists list of newly persisted entity instances */ public void handleRemovalsAndPersists(MergeContext mergeContext, List<Object> removals, List<Object> persists) { for (Object removal : removals) { Object entity = getCachedObject(removal, true); if (entity == null) // Not found in local cache, cannot remove continue; if (mergeContext.getExternalDataSessionId() != null && !mergeContext.isResolvingConflict() && dirtyCheckContext.isEntityChanged(entity)) { // Conflict between externally received data and local modifications log.error("conflict with external data removal detected on %s", ObjectUtil.toString(entity)); mergeContext.addConflict(entity, null, null); } else { boolean saveMerging = mergeContext.isMerging(); try { mergeContext.setMerging(true); List<Object[]> owners = getOwnerEntities(entity); if (owners != null) { for (Object[] owner : owners) { Object val = dataManager.getPropertyValue(owner[0], (String)owner[1]); if (val instanceof PersistentCollection && !((PersistentCollection<?>)val).wasInitialized()) continue; if (val instanceof List<?>) { List<?> list = (List<?>)val; int idx = list.indexOf(entity); if (idx >= 0) list.remove(idx); } else if (val instanceof Collection<?>) { Collection<?> coll = (Collection<?>)val; if (coll.contains(entity)) coll.remove(entity); } else if (val instanceof Map<?, ?>) { Map<?, ?> map = (Map<?, ?>)val; if (map.containsKey(entity)) map.remove(entity); for (Iterator<?> ikey = map.keySet().iterator(); ikey.hasNext(); ) { Object key = ikey.next(); if (ObjectUtil.objectEquals(dataManager, map.get(key), entity)) ikey.remove(); } } } } /* May not be necessary, should be cleaned up by weak reference */ Map<String, Object> pvalues = dataManager.getPropertyValues(entity, false, true); for (Object val : pvalues.values()) { if (val instanceof Collection<?> || val instanceof Map<?, ?> || (val != null && val.getClass().isArray())) entityReferences.remove(val); } entityReferences.remove(entity); detach(entity, new IdentityHashMap<Object, Object>(), true); } finally { mergeContext.setMerging(saveMerging); } } } dirtyCheckContext.fixRemovalsAndPersists(mergeContext, removals, persists); } private List<DataConflictListener> dataConflictListeners = new ArrayList<DataConflictListener>(); public void addListener(DataConflictListener listener) { dataConflictListeners.add(listener); } public void removeListener(DataConflictListener listener) { dataConflictListeners.remove(listener); } /** * Dispatch an event when last merge generated conflicts * * @param mergeContext current merge context */ public void handleMergeConflicts(MergeContext mergeContext) { // Clear thread cache so acceptClient/acceptServer can work inside the conflicts handler // mergeContext.clearCache(); mergeContext.initMergeConflicts(); if (mergeContext.getMergeConflicts() != null) { for (DataConflictListener listener : dataConflictListeners) listener.onConflict(this, mergeContext.getMergeConflicts()); } } /** * Resolve merge conflicts * * @param mergeContext current merge context * @param modifiedEntity the received entity * @param localEntity the locally cached entity * @param resolving true to keep client state */ public void resolveMergeConflicts(MergeContext mergeContext, Object modifiedEntity, Object localEntity, boolean resolving) { try { mergeContext.setResolvingConflict(resolving); if (modifiedEntity == null) handleRemovalsAndPersists(mergeContext, Collections.singletonList(localEntity), Collections.emptyList()); else mergeExternal(mergeContext, modifiedEntity, localEntity, null, null, false); mergeContext.checkConflictsResolved(); } finally { mergeContext.setResolvingConflict(false); } } /** * {@inheritDoc} */ public Map<Object, Map<String, Object>> getSavedProperties() { return dirtyCheckContext.getSavedProperties(); } /** * {@inheritDoc} */ public Map<String, Object> getSavedProperties(Object entity) { Object localEntity = getCachedObject(entity, true); if (localEntity == null) return null; return dirtyCheckContext.getSavedProperties(localEntity); } /** * Default implementation of entity merge for simple ActionScript beans with public properties * Can be used to implement Tide managed entities with simple objects * * @param mergeContext current merge context * @param obj source object * @param dest destination object * @param parent owning object * @param propertyName property name of the owning object */ public void defaultMerge(MergeContext mergeContext, Object obj, Object dest, Object parent, String propertyName) { // Merge internal state if (isEntity(obj)) dataManager.copyProxyState(dest, obj); // Don't merge version during conflict resolution Map<String, Object> pval = dataManager.getPropertyValues(obj, mergeContext.isResolvingConflict(), false); List<String> rw = new ArrayList<String>(); boolean isEmbedded = isEntity(parent) && !isEntity(obj); for (Entry<String, Object> mval : pval.entrySet()) { String propName = mval.getKey(); Object o = mval.getValue(); Object d = dataManager.getPropertyValue(dest, propName); o = mergeExternal(mergeContext, o, d, isEmbedded ? parent : dest, isEmbedded ? propertyName + "." + propName : propName, false); if (o != d && mergeContext.isMergeUpdate()) dataManager.setPropertyValue(dest, propName, o); rw.add(propName); } pval = dataManager.getPropertyValues(obj, mergeContext.isResolvingConflict(), true); for (Entry<String, Object> mval : pval.entrySet()) { if (rw.contains(mval.getKey())) continue; String propName = mval.getKey(); Object o = mval.getValue(); Object d = dataManager.getPropertyValue(dest, propName); if (isEntity(o) || isEntity(d)) 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."); mergeExternal(mergeContext, o, d, parent != null ? parent : dest, propertyName != null ? propertyName + '.' + propName : propName, false); } } public boolean isEntityChanged(Object entity) { return dirtyCheckContext.isEntityChanged(entity); } public boolean isEntityDeepChanged(Object entity) { return dirtyCheckContext.isEntityDeepChanged(entity); } /** * Discard changes of entity from last version received from the server * * @param entity entity to restore */ public void resetEntity(Object entity) { if (entity == null) throw new IllegalArgumentException("Entity cannot be null"); EntityManager em = PersistenceManager.getEntityManager(entity); if (em == null) return; if (em != this) throw new IllegalArgumentException("Cannot reset an entity attached to another entity manager " + entity); Set<Object> cache = new HashSet<Object>(); resetEntity(entity, cache); } private void resetEntity(Object entity, Set<Object> cache) { try { MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, null); // Disable dirty check during reset of entity mergeContext.setMerging(true); dirtyCheckContext.resetEntity(mergeContext, entity, entity, cache); } finally { MergeContext.destroy(this); } } /** * Discard changes of all cached entities from last version received from the server */ public void resetAllEntities() { try { Set<Object> cache = new HashSet<Object>(); MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, null); // Disable dirty check during reset of entity mergeContext.setMerging(true); dirtyCheckContext.resetAllEntities(mergeContext, cache); } finally { MergeContext.destroy(this); } } /** * {@inheritDoc} */ public void acceptConflict(Conflict conflict, boolean client) { Object modifiedEntity = null; if (client) { // Copy the local entity to save local changes EntityManager entityManager = PersistenceManager.getEntityManager(conflict.getLocalEntity()); EntityManager tmp = entityManager.newTemporaryEntityManager(); modifiedEntity = tmp.mergeFromEntityManager(entityManager, conflict.getServerSession(), conflict.getLocalEntity(), null, false); tmp.clear(); } else modifiedEntity = conflict.getReceivedEntity(); try { MergeContext mergeContext = new MergeContext(this, dirtyCheckContext, null); // Reset the local entity to its last stable state resetEntity(conflict.getLocalEntity()); if (client) { // Merge with the incoming entity (to update version, id and all) if (conflict.getReceivedEntity() != null) mergeExternal(mergeContext, conflict.getReceivedEntity(), conflict.getLocalEntity(), null, null, false); } // Finally reapply local changes on merged received result resolveMergeConflicts(mergeContext, modifiedEntity, conflict.getLocalEntity(), client); } finally { MergeContext.destroy(this); } } private RemoteInitializer remoteInitializer = null; @Override public void setRemoteInitializer(RemoteInitializer remoteInitializer) { this.remoteInitializer = remoteInitializer; } /** * {@inheritDoc} */ public boolean initializeObject(ServerSession serverSession, Object entity, String propertyName, Object object) { boolean initialize = false; if (remoteInitializer != null) initialize = remoteInitializer.initializeObject(serverSession, entity, propertyName, object); return initialize; } public class DefaultTrackingHandler implements DataManager.TrackingHandler { /** * Property change handler to save changes on embedded objects * * @param target changed object * @param property property name * @param oldValue old value * @param newValue new value */ public void entityPropertyChangeHandler(Object target, String property, Object oldValue, Object newValue) { MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(target)); if ((mergeContext != null && mergeContext.getSourceEntityManager() == this) || !isActive()) return; if (newValue != oldValue) { if (isEntity(oldValue) || oldValue instanceof Collection<?> || oldValue instanceof Map<?, ?>) { removeReference(oldValue, target, property); dataManager.stopTracking(oldValue, target); } if (isEntity(newValue) || newValue instanceof Collection<?> || newValue instanceof Map<?, ?>) { addReference(newValue, target, property); dataManager.startTracking(newValue, target); } } log.debug("property changed: %s %s", ObjectUtil.toString(target), property); if (mergeContext == null || !mergeContext.isMerging() || mergeContext.isResolvingConflict()) { Object owner = isEntity(target) ? null : getOwnerEntity(target); if (owner == null) dirtyCheckContext.entityPropertyChangeHandler(target, target, property, oldValue, newValue); else if (owner instanceof Object[] && isEntity(((Object[])owner)[0])) dirtyCheckContext.entityPropertyChangeHandler(((Object[])owner)[0], target, property, oldValue, newValue); } } /** * Collection change handler to save changes on collections * * @param kind change kind * @param target collection * @param location location of change * @param items changed items */ public void collectionChangeHandler(ChangeKind kind, Object target, Integer location, Object[] items) { } /** * Collection change handler to save changes on owned collections * * @param kind change kind * @param target collection * @param location location of change * @param items changed items */ public void entityCollectionChangeHandler(ChangeKind kind, Object target, Integer location, Object[] items) { MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(target)); if ((mergeContext != null && mergeContext.getSourceEntityManager() == this) || !isActive()) return; int i = 0; Object[] parent = null; if (kind == ChangeKind.ADD && items != null && items.length > 0) { parent = getOwnerEntity(target); for (i = 0; i < items.length; i++) { if (isEntity(items[i])) { if (parent != null) addReference(items[i], parent[0], (String)parent[1]); else attachEntity(items[i]); dataManager.startTracking(items[i], parent != null ? parent[0] : null); } } } else if (kind == ChangeKind.REMOVE && items != null && items.length > 0) { parent = getOwnerEntity(target); if (parent != null) { for (i = 0; i < items.length; i++) { if (isEntity(items[i])) removeReference(items[i], parent[0], (String)parent[1]); } } } else if (kind == ChangeKind.REPLACE && items != null && items.length > 0) { parent = getOwnerEntity(target); for (i = 0; i < items.length; i++) { Object newValue = ((Object[])items[i])[1]; if (isEntity(newValue)) { if (parent != null) addReference(newValue, parent[0], (String)parent[1]); else attachEntity(newValue); dataManager.startTracking(newValue, parent != null ? parent[0] : null); } } } if (!(kind == ChangeKind.ADD || kind == ChangeKind.REMOVE || kind == ChangeKind.REPLACE)) return; log.debug("collection changed: %s %s", kind, ObjectUtil.toString(target)); if (mergeContext == null || !mergeContext.isMerging() || mergeContext.isResolvingConflict()) { if (parent == null) log.warn("Owner entity not found for collection %s, cannot process dirty checking", ObjectUtil.toString(target)); else dirtyCheckContext.entityCollectionChangeHandler(parent[0], (String)parent[1], (Collection<?>)target, kind, location, items); } } /** * Map change handler to save changes on maps * * @param kind change kind * @param target collection * @param location location of change * @param items changed items */ public void mapChangeHandler(ChangeKind kind, Object target, Integer location, Object[] items) { } /** * Map change handler to save changes on owned maps * * @param kind change kind * @param target collection * @param location location of change * @param items changed items */ public void entityMapChangeHandler(ChangeKind kind, Object target, Integer location, Object[] items) { MergeContext mergeContext = MergeContext.get(PersistenceManager.getEntityManager(target)); if ((mergeContext != null && mergeContext.getSourceEntityManager() == this) || !isActive()) return; Object[] parent = null; if (kind == ChangeKind.ADD && items != null && items.length > 0) { parent = getOwnerEntity(target); for (int i = 0; i < items.length; i++) { if (isEntity(items[i])) { if (parent != null) addReference(items[i], parent[0], (String)parent[1]); else attachEntity(items[i]); dataManager.startTracking(items[i], parent != null ? parent[0] : null); } else if (items[i] instanceof Object[]) { Object[] obj = (Object[])items[i]; if (isEntity(obj[0])) { if (parent != null) addReference(obj[0], parent[0], (String)parent[1]); else attachEntity(obj[0]); dataManager.startTracking(obj[0], parent != null ? parent[0] : null); } if (isEntity(obj[1])) { if (parent != null) addReference(obj[1], parent[0], (String)parent[1]); else attachEntity(obj[1]); dataManager.startTracking(obj[1], parent != null ? parent[0] : null); } } } } else if (kind == ChangeKind.REMOVE && items != null && items.length > 0) { parent = getOwnerEntity(target); if (parent != null) { for (int i = 0; i < items.length; i++) { if (isEntity(items[i])) { removeReference(items[i], parent[0], (String)parent[1]); } else if (items[i] instanceof Object[]) { Object[] obj = (Object[])items[i]; if (isEntity(obj[0])) { removeReference(obj[0], parent[0], (String)parent[1]); } if (isEntity(obj[1])) { removeReference(obj[1], parent[0], (String)parent[1]); } } } } } else if (kind == ChangeKind.REPLACE && items != null && items.length > 0) { parent = getOwnerEntity(target); for (int i = 0; i < items.length; i++) { Object[] item = (Object[])items[i]; if (isEntity(item[1])) { if (parent != null) removeReference(item[1], parent[0], (String)parent[1]); } if (isEntity(item[2])) { if (parent != null) addReference(item[2], parent[0], (String)parent[1]); else attachEntity(item[2]); dataManager.startTracking(item[2], parent != null ? parent[0] : null); } } } if (!(kind == ChangeKind.ADD || kind == ChangeKind.REMOVE || kind == ChangeKind.REPLACE)) return; log.debug("map changed: %s %s", kind, ObjectUtil.toString(target)); if (mergeContext == null || !mergeContext.isMerging() || mergeContext.isResolvingConflict()) { if (parent == null) log.warn("Owner entity not found for collection %s, cannot process dirty checking", ObjectUtil.toString(target)); else dirtyCheckContext.entityMapChangeHandler(parent[0], (String)parent[1], (Map<?, ?>)target, kind, items); } } } /** * Handle data updates * * @param mergeContext current merge context * @param sourceSessionId sessionId from which data updates come (null when from current session) * @param updates list of data updates */ public void handleUpdates(MergeContext mergeContext, String sourceSessionId, List<Update> updates) { List<Object> merges = new ArrayList<Object>(); List<Object> removals = new ArrayList<Object>(); List<Object> persists = new ArrayList<Object>(); for (Update update : updates) { if (update.getKind() == UpdateKind.PERSIST || update.getKind() == UpdateKind.UPDATE) merges.add(update.getEntity()); else if (update.getKind() == UpdateKind.REMOVE) removals.add(update.getEntity()); if (update.getKind() == UpdateKind.PERSIST) persists.add(update.getEntity()); } mergeContext.setExternalDataSessionId(sourceSessionId); if (merges.size() == 1) internalMergeExternalData(mergeContext, merges.get(0), null, removals, persists); else if (merges.size() > 1) internalMergeExternalData(mergeContext, merges, null, removals, persists); else if (!removals.isEmpty() || !persists.isEmpty()) internalMergeExternalData(mergeContext, null, null, removals, persists); for (Update update : updates) { if (update.getEntity() instanceof String) continue; Object entity = getCachedObject(update.getEntity(), update.getKind() != UpdateKind.REMOVE); if (entity != null) update.setEntity(entity); } } public void raiseUpdateEvents(Context context, List<EntityManager.Update> updates) { List<String> refreshes = new ArrayList<String>(); for (EntityManager.Update update : updates) { Object entity = update.getEntity(); if (update.getKind() == UpdateKind.REFRESH) { String entityName = getUnqualifiedClassName((String)entity); refreshes.add(entityName); } else if (entity != null) { String entityName = entity instanceof EntityRef ? getUnqualifiedClassName(((EntityRef)entity).getClassName()) : entity.getClass().getSimpleName(); String eventType = update.getKind().eventName() + "." + entityName; context.getEventBus().raiseEvent(context, eventType, entity); if (UpdateKind.PERSIST.equals(update.getKind()) || UpdateKind.REMOVE.equals(update.getKind())) { if (!refreshes.contains(entityName)) refreshes.add(entityName); } } } for (String refresh : refreshes) context.getEventBus().raiseEvent(context, UpdateKind.REFRESH.eventName() + "." + refresh); } private static String getUnqualifiedClassName(String className) { int idx = className.lastIndexOf("."); return idx >= 0 ? className.substring(idx+1) : className; } @Override public void setRemoteValidator(RemoteValidator remoteValidator) { } @Override public boolean validateObject(Object object, String property, Object value) { return false; } }