/* GRANITE DATA SERVICES Copyright (C) 2012 GRANITE DATA SERVICES S.A.S. This file is part of Granite Data Services. Granite Data Services is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Granite Data Services 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, see <http://www.gnu.org/licenses/>. */ package org.granite.client.tide.data.impl; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; 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 org.granite.client.persistence.collection.PersistentCollection; import org.granite.client.tide.PropertyHolder; import org.granite.client.tide.data.spi.DataManager; import org.granite.client.tide.data.spi.DataManager.ChangeKind; import org.granite.client.tide.data.spi.DirtyCheckContext; import org.granite.client.tide.data.spi.ExpressionEvaluator.Value; import org.granite.client.tide.data.spi.MergeContext; import org.granite.client.tide.data.spi.Wrapper; import org.granite.client.tide.server.TrackingContext; import org.granite.client.util.WeakIdentityHashMap; import org.granite.logging.Logger; /** * @author William DRAI */ public class DirtyCheckContextImpl implements DirtyCheckContext { private static Logger log = Logger.getLogger("org.granite.client.tide.data.DirtyCheckContextImpl"); private DataManager dataManager; private TrackingContext trackingContext; private int dirtyCount = 0; private WeakIdentityHashMap<Object, Map<String, Object>> savedProperties = new WeakIdentityHashMap<Object, Map<String, Object>>(); private WeakIdentityHashMap<Object, Object> unsavedEntities = new WeakIdentityHashMap<Object, Object>(); public DirtyCheckContextImpl(DataManager dataManager, TrackingContext trackingContext) { this.dataManager = dataManager; this.trackingContext = trackingContext; } @Override public void setTrackingContext(TrackingContext trackingContext) { this.trackingContext = trackingContext; } private boolean isEntity(Object obj) { return dataManager.isEntity(obj); } public boolean isDirty() { return dirtyCount > 0; } public void notifyDirtyChange(boolean oldDirty) { if (isDirty() == oldDirty) return; dataManager.notifyDirtyChange(oldDirty, isDirty()); } public boolean notifyEntityDirtyChange(Object entity, boolean oldDirtyEntity) { boolean newDirtyEntity = isEntityChanged(entity); if (newDirtyEntity != oldDirtyEntity) dataManager.notifyEntityDirtyChange(entity, oldDirtyEntity, newDirtyEntity); return newDirtyEntity; } public Map<String, Object> getSavedProperties(Object entity) { return savedProperties.get(entity); } public boolean isSaved(Object entity) { return savedProperties.containsKey(entity); } /** * Check if the object is marked as new in the context * * @param object object to check * * @return true if the object has been newly attached */ public boolean isUnsaved(Object object) { return unsavedEntities.containsKey(object); } public void addUnsaved(Object entity) { unsavedEntities.put(entity, true); } public void clear(boolean notify) { boolean wasDirty = isDirty(); dirtyCount = 0; savedProperties.clear(); if (notify) notifyDirtyChange(wasDirty); } /** * Check if entity property has been changed since last remote call * * @param entity entity to check * @param propertyName property to check * @param value current value to compare with saved value * * @return true is value has been changed */ public boolean isEntityPropertyChanged(Object entity, String propertyName, Object value) { Map<String, Object> source = savedProperties.get(entity); if (source != null) return source.containsKey(propertyName) && !isSame(source.get(propertyName), value); return !isSame(dataManager.getPropertyValue(entity, propertyName), value); } public boolean isEntityChanged(Object entity) { return isEntityChanged(entity, null, null, null); } /** * Check if entity has changed since last save point * * @param entity entity to check * @param propName property name * @param value * * @return entity is dirty */ @SuppressWarnings("unchecked") public boolean isEntityChanged(Object entity, Object embedded, String propName, Object value) { if (!dataManager.isInitialized(entity)) return false; boolean saveTracking = trackingContext.isEnabled(); try { trackingContext.setEnabled(false); boolean dirty = false; Map<String, Object> pval = dataManager.getPropertyValues(entity, true, false); Map<String, Object> save = savedProperties.get(entity); if (embedded == null) embedded = entity; for (String p : pval.keySet()) { Object val = (entity == embedded && p.equals(propName)) ? value : pval.get(p); Object saveval = save != null ? save.get(p) : null; if (save != null && ((val != null && (ObjectUtil.isSimple(val) || val instanceof byte[])) || (saveval != null && (ObjectUtil.isSimple(saveval) || saveval instanceof byte[])))) { dirty = true; break; } else if (save != null && (val instanceof Value || saveval instanceof Value || val instanceof Enum || saveval instanceof Enum)) { if (saveval != null && ((val == null && saveval != null) || !val.equals(saveval))) { dirty = true; break; } } else if (save != null && (isEntity(val) || isEntity(saveval))) { if (saveval != null && val != save.get(p)) { dirty = true; break; } } else if ((val instanceof Collection<?> || val instanceof Map<?, ?>)) { if (!dataManager.isInitialized(val)) continue; List<Change> savedArray = (List<Change>)saveval; if (savedArray != null && !savedArray.isEmpty()) { dirty = true; break; } } else if (val != null && !(isEntity(val) || ObjectUtil.isSimple(val) || val instanceof Enum || val instanceof Value || val instanceof byte[]) && isEntityChanged(val)) { dirty = true; break; } } return dirty; } finally { trackingContext.setEnabled(saveTracking); } } public boolean isEntityDeepChanged(Object entity) { return isEntityDeepChanged(entity, null, new IdentityHashMap<Object, Boolean>()); } private boolean isEntityDeepChanged(Object entity, Object embedded, IdentityHashMap<Object, Boolean> cache) { if (cache == null) cache = new IdentityHashMap<Object, Boolean>(); if (cache.containsKey(entity)) return false; cache.put(entity, true); if (!dataManager.isInitialized(entity)) return false; boolean saveTracking = trackingContext.isEnabled(); try { trackingContext.setEnabled(false); Map<String, Object> pval = dataManager.getPropertyValues(entity, true, false); if (embedded == null) embedded = entity; Map<String, Object> save = savedProperties.get(entity); for (String p : pval.keySet()) { Object val = pval.get(p); Object saveval = save != null ? save.get(p) : null; if (save != null && ((val != null && (ObjectUtil.isSimple(val) || val instanceof byte[])) || (saveval != null && (ObjectUtil.isSimple(saveval) || saveval instanceof byte[])))) { return true; } else if (save != null && (val instanceof Value || saveval instanceof Value || val instanceof Enum || saveval instanceof Enum)) { if (saveval != null && ((val == null && saveval != null) || !val.equals(saveval))) { return true; } } else if (save != null && (isEntity(val) || isEntity(saveval))) { if (saveval != null && val != save.get(p)) return true; if (isEntityDeepChanged(val, null, cache)) return true; } else if (val instanceof Collection<?> || val instanceof Map<?, ?>) { if (!dataManager.isInitialized(val)) continue; @SuppressWarnings("unchecked") List<Change> savedArray = (List<Change>)saveval; if (savedArray != null && !savedArray.isEmpty()) return true; if (val instanceof Collection<?>) { for (Object elt : (Collection<?>)val) { if (isEntityDeepChanged(elt, null, cache)) return true; } } else if (val instanceof Map<?, ?>) { for (Entry<?, ?> me : ((Map<?, ?>)val).entrySet()) { if (isEntityDeepChanged(me.getKey(), null, cache)) return true; if (isEntityDeepChanged(me.getValue(), null, cache)) return true; } } } else if (val != null && !(isEntity(val) || ObjectUtil.isSimple(val) || val instanceof Enum || val instanceof Value || val instanceof byte[]) && isEntityDeepChanged(val, embedded, cache)) { return true; } } } finally { trackingContext.setEnabled(saveTracking); } return false; } private boolean isSame(Object val1, Object val2) { if (val1 == null && isEmpty(val2)) return true; else if (val2 == null && isEmpty(val1)) return true; else if (ObjectUtil.isSimple(val1) && ObjectUtil.isSimple(val2)) return val1.equals(val2); else if (val1 instanceof byte[] && val2 instanceof byte[]) return Arrays.equals((byte[])val1, (byte[])val2); else if ((val1 instanceof Value && val2 instanceof Value) || (val1 instanceof Enum && val2 instanceof Enum)) return val1.equals(val2); Object n = val1 instanceof Wrapper ? ((Wrapper)val1).getWrappedObject() : val1; Object o = val2 instanceof Wrapper ? ((Wrapper)val2).getWrappedObject() : val2; if (isEntity(n) && isEntity(o)) return dataManager.getUid(n) != null && dataManager.getUid(n).equals(dataManager.getUid(o)); return n == o; } private boolean isSameList(List<Object> save, Collection<?> coll) { if (save.size() != coll.size()) return false; if (coll instanceof List<?>) { List<?> list = (List<?>)coll; for (int i = 0; i < save.size(); i++) { if (!isSame(save.get(i), list.get(i))) return false; } } else { for (int i = 0; i < save.size(); i++) { boolean found = false; for (Iterator<?> ic = coll.iterator(); ic.hasNext(); ) { Object obj = ic.next(); if (isSame(save.get(i), obj)) { found = true; break; } } if (!found) return false; } } return true; } private boolean isSameMap(List<Object[]> save, Map<?, ?> map) { if (save.size() != map.size()) return false; for (int i = 0; i < save.size(); i++) { Object[] entry = save.get(i); if (!map.containsKey(entry[0])) return false; if (!isSame(entry[1], map.get(entry[0]))) return false; } return true; } private boolean isSameExt(Object val1, Object val2) { if (val1 == null && isEmpty(val2)) return true; else if (val2 == null && isEmpty(val1)) return true; else if (ObjectUtil.isSimple(val1) && ObjectUtil.isSimple(val2)) return val1.equals(val2); else if (val1 instanceof byte[] && val2 instanceof byte[]) return Arrays.equals((byte[])val1, (byte[])val2); else if ((val1 instanceof Value && val2 instanceof Value) || (val1 instanceof Enum && val2 instanceof Enum)) return val1.equals(val2); else if (val1 != null && val1.getClass().isArray() && val2 != null && val2.getClass().isArray()) { if (Array.getLength(val1) != Array.getLength(val2)) return false; for (int idx = 0; idx < Array.getLength(val1); idx++) { if (!isSameExt(Array.get(val1, idx), Array.get(val2, idx))) return false; } return true; } else if (val1 instanceof Set<?> && val2 instanceof Set<?>) { if ((val1 instanceof PersistentCollection && !((PersistentCollection)val1).wasInitialized()) || (val2 instanceof PersistentCollection && !((PersistentCollection)val2).wasInitialized())) return false; Collection<?> coll1 = (Collection<?>)val1; Collection<?> coll2 = (Collection<?>)val2; if (coll1.size() != coll2.size()) return false; for (Object e : coll1) { boolean found = false; for (Object f : coll2) { if (isSameExt(e, f)) { found = true; break; } } if (!found) return false; } for (Object e : coll2) { boolean found = false; for (Object f : coll1) { if (isSameExt(e, f)) { found = true; break; } } if (!found) return false; } return true; } else if (val1 instanceof List<?> && val2 instanceof List<?>) { if ((val1 instanceof PersistentCollection && !((PersistentCollection)val1).wasInitialized()) || (val2 instanceof PersistentCollection && !((PersistentCollection)val2).wasInitialized())) return false; List<?> list1 = (List<?>)val1; List<?> list2 = (List<?>)val2; if (list1.size() != list2.size()) return false; for (int idx = 0; idx < list1.size(); idx++) { if (!isSameExt(list1.get(idx), list2.get(idx))) return false; } return true; } else if (val1 instanceof Collection<?> && val2 instanceof Collection<?>) { if ((val1 instanceof PersistentCollection && !((PersistentCollection)val1).wasInitialized()) || (val2 instanceof PersistentCollection && !((PersistentCollection)val2).wasInitialized())) return false; Collection<?> coll1 = (Collection<?>)val1; Collection<?> coll2 = (Collection<?>)val2; if (coll1.size() != coll2.size()) return false; for (Object obj1 : coll1) { boolean found = false; for (Object obj2 : coll2) { if (isSameExt(obj1, obj2)) { found = true; break; } } if (!found) return false; } for (Object obj2 : coll2) { boolean found = false; for (Object obj1 : coll1) { if (isSameExt(obj1, obj2)) { found = true; break; } } if (!found) return false; } return true; } else if (val1 instanceof Map<?, ?> && val2 instanceof Map<?, ?>) { if ((val1 instanceof PersistentCollection && !((PersistentCollection)val1).wasInitialized()) || (val2 instanceof PersistentCollection && !((PersistentCollection)val2).wasInitialized())) return false; Map<?, ?> map1 = (Map<?, ?>)val1; Map<?, ?> map2 = (Map<?, ?>)val2; if (map1.size() != map2.size()) return false; for (Object e : map1.keySet()) { Object key = null; for (Object f : map2.keySet()) { if (isSameExt(e, f)) { key = f; break; } } if (key == null) return false; if (!isSameExt(map1.get(e), map2.get(key))) return false; } for (Object f : map2.keySet()) { Object key = null; for (Object e : map1.keySet()) { if (isSameExt(e, f)) { key = e; break; } } if (key == null) return false; if (!isSameExt(map1.get(key), map2.get(f))) return false; } return true; } Object n = val1 instanceof Wrapper ? ((Wrapper)val1).getWrappedObject() : val1; Object o = val2 instanceof Wrapper ? ((Wrapper)val2).getWrappedObject() : val2; if (isEntity(n) && isEntity(o)) return dataManager.getUid(n).equals(dataManager.getUid(o)); return n == o; } /** * Interceptor for managed entity setters * * @param entity entity to intercept * @param propName property name * @param oldValue old value * @param newValue new value */ public void entityPropertyChangeHandler(Object entity, Object object, String propName, Object oldValue, Object newValue) { boolean oldDirty = isDirty(); boolean diff = !isSame(oldValue, newValue); if (diff) { boolean oldDirtyEntity = isEntityChanged(entity, object, propName, oldValue); String versionPropertyName = dataManager.isEntity(entity) ? dataManager.getVersionPropertyName(entity) : null; Map<String, Object> save = savedProperties.get(object); boolean unsaved = save == null; if (unsaved || (versionPropertyName != null && (save.get(versionPropertyName) != dataManager.getVersion(entity) && !(save.get(versionPropertyName) == null && dataManager.getVersion(entity) == null)))) { save = new HashMap<String, Object>(); if (versionPropertyName != null) save.put(versionPropertyName, dataManager.getVersion(entity)); savedProperties.put(object, save); save.put(propName, oldValue); if (unsaved) dirtyCount++; } if (save != null && (versionPropertyName == null || save.get(versionPropertyName) == dataManager.getVersion(entity) || (save.get(versionPropertyName) == null && dataManager.getVersion(entity) == null))) { if (!save.containsKey(propName)) save.put(propName, oldValue); if (isSame(save.get(propName), newValue)) { save.remove(propName); int count = 0; for (String p : save.keySet()) { if (!p.equals(versionPropertyName)) count++; } if (count == 0) { savedProperties.remove(object); dirtyCount--; } } } notifyEntityDirtyChange(entity, oldDirtyEntity); } notifyDirtyChange(oldDirty); } /** * Collection event handler to save changes on managed collections * * @param owner owner entity of the collection * @param propName property name of the collection * @param coll collection * @param kind change kind * @param location change location * @param items changed items */ @SuppressWarnings("unchecked") public void entityCollectionChangeHandler(Object owner, String propName, Collection<?> coll, ChangeKind kind, Integer location, Object[] items) { boolean oldDirty = isDirty(); String versionPropertyName = dataManager.isEntity(owner) ? dataManager.getVersionPropertyName(owner) : null; boolean oldDirtyEntity = isEntityChanged(owner); Map<String, Object> esave = savedProperties.get(owner); boolean unsaved = esave == null; if (unsaved || (versionPropertyName != null && (esave.get(versionPropertyName) != dataManager.getVersion(owner) && !(esave.get(versionPropertyName) == null && dataManager.getVersion(owner) == null)))) { esave = new HashMap<String, Object>(); if (versionPropertyName != null) esave.put(versionPropertyName, dataManager.getVersion(owner)); savedProperties.put(owner, esave); if (unsaved) dirtyCount++; } List<Object> save = (List<Object>)esave.get(propName); if (save == null) { save = new ArrayList<Object>(); esave.put(propName, save); // Save collection snapshot for (Object e : coll) save.add(e); // Adjust with last event if (kind == ChangeKind.ADD) { if (location != null) { for (int i = 0; i < items.length; i++) save.remove(location.intValue()); } else { for (Object item : items) save.remove(item); } } else if (kind == ChangeKind.REMOVE) { if (location != null) { for (int i = 0; i < items.length; i++) save.add(location.intValue()+i, items[i]); } else { for (Object item : items) save.add(item); } } else if (kind == ChangeKind.REPLACE) { if (location != null) save.set(location.intValue(), ((Object[])items[0])[0]); else { save.remove(((Object[])items[0])[1]); save.add(((Object[])items[0])[0]); } } } else { if (isSameList(save, coll)) { esave.remove(propName); int count = 0; for (Object p : esave.keySet()) { if (!p.equals(versionPropertyName)) count++; } if (count == 0) { savedProperties.remove(owner); dirtyCount--; } } } notifyEntityDirtyChange(owner, oldDirtyEntity); notifyDirtyChange(oldDirty); } /** * Map event handler to save changes on managed maps * * @param owner owner entity of the map * @param propName property name of the map * @param map map * @param kind change kind * @param items changed items */ @SuppressWarnings("unchecked") public void entityMapChangeHandler(Object owner, String propName, Map<?, ?> map, ChangeKind kind, Object[] items) { boolean oldDirty = isDirty(); String versionPropertyName = dataManager.isEntity(owner) ? dataManager.getVersionPropertyName(owner) : null; boolean oldDirtyEntity = isEntityChanged(owner); Map<String, Object> esave = savedProperties.get(owner); boolean unsaved = esave == null; if (unsaved || (versionPropertyName != null && (esave.get(versionPropertyName) != dataManager.getVersion(owner) && !(esave.get(versionPropertyName) == null && dataManager.getVersion(owner) == null)))) { esave = new HashMap<String, Object>(); if (versionPropertyName != null) esave.put(versionPropertyName, dataManager.getVersion(owner)); savedProperties.put(owner, esave); if (unsaved) dirtyCount++; } List<Object[]> save = (List<Object[]>)esave.get(propName); if (save == null) { save = new ArrayList<Object[]>(); esave.put(propName, save); // Save map snapshot for (Entry<?, ?> entry : map.entrySet()) { boolean found = false; if (kind == ChangeKind.ADD) { for (Object item : items) { if (isSame(entry.getKey(), ((Object[])item)[0])) { found = true; break; } } } else if (kind == ChangeKind.REPLACE) { for (Object item : items) { if (isSame(entry.getKey(), ((Object[])item)[0])) { save.add(new Object[] { entry.getKey(), ((Object[])item)[1] }); found = true; break; } } } if (!found) save.add(new Object[] { entry.getKey(), entry.getValue() }); } // Add removed element if needed if (kind == ChangeKind.REMOVE) { for (Object item : items) save.add(new Object[] { ((Object[])item)[0], ((Object[])item)[1] }); } } else { if (isSameMap(save, map)) { esave.remove(propName); int count = 0; for (Object p : esave.keySet()) { if (!p.equals(versionPropertyName)) count++; } if (count == 0) { savedProperties.remove(owner); dirtyCount--; } } } notifyEntityDirtyChange(owner, oldDirtyEntity); notifyDirtyChange(oldDirty); } /** * Mark an object merged from the server as not dirty * * @param object merged object * @param entity owner entity (when handling embedded objects) */ public void markNotDirty(Object object, Object entity) { if (entity != null) unsavedEntities.remove(entity); if (!savedProperties.containsKey(object)) return; boolean oldDirty = isDirty(); boolean oldDirtyEntity = false; if (entity == null && isEntity(object)) entity = object; if (entity != null) oldDirtyEntity = isEntityChanged(entity); savedProperties.remove(object); if (entity != null) notifyEntityDirtyChange(entity, oldDirtyEntity); dirtyCount--; notifyDirtyChange(oldDirty); } /** * Check if dirty properties of an object are the same than those of another entity * When they are the same, unmark the dirty flag * * @param mergeContext current merge context * @param entity merged entity * @param source source entity * @param parent owner entity for embedded objects * @return true if the entity is still dirty after comparing with incoming object */ public boolean checkAndMarkNotDirty(MergeContext mergeContext, Object entity, Object source, Object parent) { if (entity != null) unsavedEntities.remove(entity); Map<String, Object> save = savedProperties.get(entity); if (save == null) return false; Object owner = isEntity(entity) ? entity : parent; boolean oldDirty = isDirty(); boolean oldDirtyEntity = isEntityChanged(owner); List<String> merged = new ArrayList<String>(); String versionPropertyName = dataManager.getVersionPropertyName(owner); if (isEntity(source) && versionPropertyName != null) save.put(versionPropertyName, dataManager.getVersion(source)); Map<String, Object> pval = dataManager.getPropertyValues(entity, false, false); for (String propName : pval.keySet()) { if (propName.equals(versionPropertyName) || propName.equals("dirty")) continue; // if (!source.hasOwnProperty(propName)) // continue; Object localValue = pval.get(propName); if (localValue instanceof PropertyHolder) localValue = ((PropertyHolder)localValue).getObject(); Object sourceValue = dataManager.getPropertyValue(source, propName); if (isSameExt(sourceValue, localValue)) { merged.add(propName); continue; } if (sourceValue == null || ObjectUtil.isSimple(sourceValue) || sourceValue instanceof Value || sourceValue instanceof Enum) { save.put(propName, sourceValue); } else if (isEntity(sourceValue)) { save.put(propName, mergeContext.getFromCache(sourceValue)); } else if (sourceValue instanceof Collection<?> && !(sourceValue instanceof PersistentCollection && !((PersistentCollection)sourceValue).wasInitialized())) { List<Object> snapshot = new ArrayList<Object>((Collection<?>)sourceValue); save.put(propName, snapshot); } else if (sourceValue instanceof Map<?, ?> && !(sourceValue instanceof PersistentCollection && !((PersistentCollection)sourceValue).wasInitialized())) { Map<?, ?> map = (Map<?, ?>)sourceValue; List<Object[]> snapshot = new ArrayList<Object[]>(map.size()); for (Entry<?, ?> entry : map.entrySet()) snapshot.add(new Object[] { entry.getKey(), entry.getValue() }); save.put(propName, snapshot); } } for (String propName : merged) save.remove(propName); int count = 0; for (String propName : save.keySet()) { if (!propName.equals(versionPropertyName)) count++; } if (count == 0) { savedProperties.remove(entity); dirtyCount--; } boolean newDirtyEntity = notifyEntityDirtyChange(owner, oldDirtyEntity); notifyDirtyChange(oldDirty); return newDirtyEntity; } public void fixRemovalsAndPersists(MergeContext mergeContext, List<Object> removals, List<Object> persists) { boolean oldDirty = dirtyCount > 0; for (Object object : savedProperties.keySet()) { Object owner = null; if (isEntity(object)) owner = object; else { Object[] ownerEntity = mergeContext.getOwnerEntity(object); if (ownerEntity != null && isEntity(ownerEntity[0])) owner = ownerEntity[0]; } String versionPropertyName = dataManager.getVersionPropertyName(owner); boolean oldDirtyEntity = isEntityChanged(object); Map<String, Object> save = savedProperties.get(object); Iterator<String> ip = save.keySet().iterator(); while (ip.hasNext()) { String p = ip.next(); Object sn = save.get(p); if (!(sn instanceof List<?>)) continue; Object value = dataManager.getPropertyValue(object, p); if (value instanceof Collection<?>) { @SuppressWarnings("unchecked") List<Object> snapshot = (List<Object>)sn; Collection<?> coll = (Collection<?>)value; if (removals != null) { Iterator<Object> isne = snapshot.iterator(); while (isne.hasNext()) { Object sne = isne.next(); for (Object removal : removals) { if (isEntity(sne) && ObjectUtil.objectEquals(dataManager, sne, removal)) isne.remove(); } } } if (persists != null) { for (Object persist : persists) { if (coll instanceof List<?>) { List<?> list = (List<?>)coll; List<Integer> found = new ArrayList<Integer>(); for (int j = 0; j < list.size(); j++) { if (ObjectUtil.objectEquals(dataManager, list.get(j), persist)) found.add(j); } for (int j = 0; j < snapshot.size(); j++) { if (ObjectUtil.objectEquals(dataManager, persist, snapshot.get(j))) { snapshot.remove(j); j--; } } for (int idx : found) snapshot.add(idx, persist); } else { if (coll.contains(persist) && !snapshot.contains(persist)) snapshot.add(persist); } } } if (isSameList(snapshot, coll)) ip.remove(); } else if (value instanceof Map<?, ?>) { @SuppressWarnings("unchecked") List<Object[]> snapshot = (List<Object[]>)sn; Map<?, ?> map = (Map<?, ?>)value; if (removals != null) { Iterator<Object[]> isne = snapshot.iterator(); while (isne.hasNext()) { Object[] sne = isne.next(); for (Object removal : removals) { if (isEntity(sne[0]) && ObjectUtil.objectEquals(dataManager, sne[0], removal)) isne.remove(); else if (isEntity(sne[1]) && ObjectUtil.objectEquals(dataManager, sne[1], removal)) isne.remove(); } } } // TODO: persist ? // if (persists != null) { // for (Object persist : persists) { // boolean foundKey = false; // List<Object> foundValues = new ArrayList<Object>(); // Iterator<Object[]> isne = snapshot.iterator(); // while (isne.hasNext()) { // Object[] sne = isne.next(); // if (ObjectUtil.objectEquals(dataManager, sne[0], persist)) // foundKey = true; // if (ObjectUtil.objectEquals(dataManager, sne[1], persist)) { // foundValues.add(sne[0]); // isne.remove(); // } // } // if (map.containsKey(persist) && !foundKey) // snapshot.add(new Object[] { persist, map.get(persist) }); // if (map.containsValue(persist)) { // for (Entry<?, ?> e : map.entrySet()) { // if (e.getValue().equals(persist) && !e.getKey().equals(persist)) // snapshot.add(new Object[] { e.getKey(), e.getValue() }); // } // } // } // } if (isSameMap(snapshot, map)) ip.remove(); } } int count = 0; for (Object p : save.keySet()) { if (!p.equals(versionPropertyName)) count++; } if (count == 0) { savedProperties.remove(object); dirtyCount--; } notifyEntityDirtyChange(object, oldDirtyEntity); } notifyDirtyChange(oldDirty); } /** * Internal implementation of entity reset */ @SuppressWarnings("unchecked") public void resetEntity(MergeContext mergeContext, Object entity, Object parent, Set<Object> cache) { // Should not try to reset uninitialized entities if (!dataManager.isInitialized(entity)) return; if (cache.contains(entity)) return; cache.add(entity); Map<String, Object> save = savedProperties.get(entity); Map<String, Object> pval = dataManager.getPropertyValues(entity, true, false); for (String p : pval.keySet()) { Object val = pval.get(p); if (val instanceof Collection<?> && dataManager.isInitialized(val)) { Collection<Object> coll = (Collection<Object>)val; List<Object> savedArray = save != null ? (List<Object>)save.get(p) : null; if (savedArray != null) { for (Object obj : coll) { if (isEntity(obj)) resetEntity(mergeContext, obj, parent, cache); } coll.clear(); for (Object e : savedArray) coll.add(e); // Must be here because collection reset may have triggered other useless collection change events markNotDirty(val, parent); } for (Object o : coll) { if (isEntity(o)) resetEntity(mergeContext, o, o, cache); } } else if (val instanceof Map<?, ?> && dataManager.isInitialized(val)) { Map<Object, Object> map = (Map<Object, Object>)val; List<Object[]> savedArray = save != null ? (List<Object[]>)save.get(p) : null; if (savedArray != null) { for (Entry<Object, Object> entry : map.entrySet()) { if (isEntity(entry.getKey())) resetEntity(mergeContext, entry.getKey(), parent, cache); if (isEntity(entry.getValue())) resetEntity(mergeContext, entry.getValue(), parent, cache); } map.clear(); for (Object[] e : savedArray) map.put(e[0], e[1]); // Must be here because collection reset has triggered other useless CollectionEvents markNotDirty(val, parent); } for (Entry<Object, Object> me : map.entrySet()) { if (isEntity(me.getKey())) resetEntity(mergeContext, me.getKey(), me.getKey(), cache); if (isEntity(me.getValue())) resetEntity(mergeContext, me.getValue(), me.getValue(), cache); } } else if (save != null && (ObjectUtil.isSimple(val) || ObjectUtil.isSimple(save.get(p)))) { if (save.containsKey(p)) dataManager.setPropertyValue(entity, p, save.get(p)); } else if (save != null && (val instanceof Enum || save.get(p) instanceof Enum || val instanceof Value || save.get(p) instanceof Value)) { if (save.containsKey(p)) dataManager.setPropertyValue(entity, p, save.get(p)); } else if (save != null && save.containsKey(p)) { if (!ObjectUtil.objectEquals(dataManager, val, save.get(p))) dataManager.setPropertyValue(entity, p, save.get(p)); } else if (isEntity(val)) resetEntity(mergeContext, val, val, cache); else if (val != null && parent != null && !ObjectUtil.isSimple(val) && !(val instanceof Enum)) resetEntity(mergeContext, val, parent, cache); } // Must be here because entity reset may have triggered useless new saved properties markNotDirty(entity, null); } /** * Internal implementation of entity reset all * * @param mergeContext current merge context * @param cache graph traversal cache */ public void resetAllEntities(MergeContext mergeContext, Set<Object> cache) { boolean found = false; do { found = false; for (Object entity : savedProperties.keySet()) { if (isEntity(entity)) { found = true; resetEntity(mergeContext, entity, entity, cache); break; } } } while (found); if (dirtyCount > 0) log.error("Incomplete reset of context, could be a bug"); } /** * Check if a value is empty * * @param val value * @return value is empty */ public boolean isEmpty(Object val) { if (val == null) return true; else if (val instanceof String) return val.equals(""); else if (val.getClass().isArray()) return Array.getLength(val) == 0; else if (val instanceof Date) return ((Date)val).getTime() == 0L; else if (val instanceof Collection<?>) return ((Collection<?>)val).size() == 0; else if (val instanceof Map<?, ?>) return ((Map<?, ?>)val).size() == 0; return false; } public static class Change { private ChangeKind kind; private int location; private Object[] items; public Change(ChangeKind kind, int location, Object[] items) { this.kind = kind; this.location = location; this.items = items; } public ChangeKind getKind() { return kind; } public int getLocation() { return location; } public Object[] getItems() { return items; } public void moveLocation(int offset) { location += offset; } } }