/**
* 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;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.granite.client.persistence.collection.PersistentCollection;
import org.granite.client.tide.data.impl.ChangeEntityRef;
import org.granite.client.tide.data.impl.ChangeProxy;
import org.granite.client.tide.data.impl.ObjectUtil;
import org.granite.client.tide.data.spi.EntityRef;
import org.granite.client.tide.data.spi.MergeContext;
import org.granite.client.util.PropertyHolder;
import org.granite.logging.Logger;
import org.granite.tide.data.Change;
import org.granite.tide.data.ChangeRef;
import org.granite.tide.data.ChangeSet;
import org.granite.tide.data.CollectionChange;
import org.granite.tide.data.CollectionChanges;
import org.granite.util.TypeUtil;
/**
* Created by william on 10/01/14.
*/
public class ChangeMerger implements DataMerger {
private static Logger log = Logger.getLogger(ChangeMerger.class);
/**
* Should return true if this merger is able to handle the specified object
*
* @param obj an object
* @return true if object can be handled
*/
public boolean accepts(Object obj) {
return obj instanceof ChangeSet || obj instanceof Change;
}
private boolean isForEntity(MergeContext mergeContext, Change change, Object entity) {
String className = mergeContext.getServerSession().getAliasRegistry().getTypeForAlias(change.getClassName());
return entity.getClass().getName().equals(className) && change.getUid().equals(mergeContext.getDataManager().getUid(entity));
}
private boolean isForEntity(MergeContext mergeContext, ChangeRef changeRef, Object entity) {
String className = mergeContext.getServerSession().getAliasRegistry().getTypeForAlias(changeRef.getClassName());
return entity.getClass().getName().equals(className) && changeRef.getUid().equals(mergeContext.getDataManager().getUid(entity));
}
/**
* Merge an entity coming from the server in the entity manager
*
* @param mergeContext current merge context
* @param changeSet incoming change/changeSet
* @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 collection in the owner object
*
* @return merged entity (=== previous when previous not null)
*/
@SuppressWarnings("unchecked")
public Object merge(MergeContext mergeContext, Object changeSet, Object previous, Object parent, String propertyName) {
if (changeSet != null || previous != null)
log.debug("merge Change: %s previous %s (change)", ObjectUtil.toString(changeSet), ObjectUtil.toString(previous));
Object next = null;
// Local ChangeSet should not be replaced by its context value
if (changeSet instanceof ChangeSet && ((ChangeSet)changeSet).isLocal())
next = changeSet;
Change[] changes = changeSet instanceof ChangeSet ? ((ChangeSet)changeSet).getChanges() : new Change[] { (Change)changeSet };
boolean local = changeSet instanceof ChangeSet ? ((ChangeSet)changeSet).isLocal() : ((Change)changeSet).isLocal();
for (Change change : changes) {
if (change.isLocal() && next == null) {
// Changes built locally must not be replaced merged
next = change;
}
ChangeEntityRef changeEntityRef = new ChangeEntityRef(change, mergeContext.getServerSession().getAliasRegistry());
Object dest = mergeContext.getCachedObject(changeEntityRef);
if (dest == null) {
// Entity not found locally : nothing to do, we can't apply incremental changes
log.warn("Incoming change received for unknown entity %s: %s", change.getClassName(), change.getUid());
continue;
}
if (dest != previous && previous != null && !isForEntity(mergeContext, change, previous)) {
// Cannot apply changes if provided change has not the same uid than the cached object
continue;
}
boolean saveSkipDirtyCheck, saveUninitAllowed;
if (local) {
saveSkipDirtyCheck = mergeContext.isSkipDirtyCheck();
saveUninitAllowed = mergeContext.isUninitializeAllowed();
try {
mergeContext.setSkipDirtyCheck(true);
mergeContext.setUninitializeAllowed(false);
// Changes built locally just need to have their referenced content merged
// to initialize their uid and attach them to the local context
for (Map.Entry<String, Object> me : change.getChanges().entrySet()) {
String p = me.getKey();
Object val = me.getValue();
if (val instanceof CollectionChanges) {
for (CollectionChange cc : ((CollectionChanges)val).getChanges()) {
if (cc.getKey() != null && !(cc.getKey() instanceof EntityRef))
mergeContext.mergeExternal(cc.getKey(), null, dest, p);
if (cc.getValue() != null && !(cc.getValue() instanceof EntityRef))
mergeContext.mergeExternal(cc.getValue(), null, dest, p);
}
}
else
mergeContext.mergeExternal(val, null, dest, p);
}
}
finally {
mergeContext.setUninitializeAllowed(saveUninitAllowed);
mergeContext.setSkipDirtyCheck(saveSkipDirtyCheck);
}
continue;
}
if (next == null)
next = dest;
saveUninitAllowed = mergeContext.isUninitializeAllowed();
try {
mergeContext.setUninitializeAllowed(false);
Map<String, Object> mergedChanges = new HashMap<String, Object>();
Object templateObject = TypeUtil.newInstance(dest.getClass(), dest.getClass());
Object incomingEntity = lookupEntity(mergeContext, change.getChanges(), dest, null);
// Create an entity proxy for the current processed target and apply changes on it
for (Map.Entry<String, Object> me : change.getChanges().entrySet()) {
String p = me.getKey();
Object val = me.getValue();
if (val instanceof CollectionChanges) {
Object coll = mergeContext.getDataManager().getPropertyValue(dest, p);
if (coll instanceof PersistentCollection && !((PersistentCollection<?>)coll).wasInitialized()) {
// Cannot update an uninitialized collection
log.debug("Incoming change for uninitialized collection %s:%s.%s", change.getClassName(), change.getUid(), p);
continue;
}
String cacheKey = "CollChange::" + dest.getClass().getName() + ":" + mergeContext.getDataManager().getUid(dest) + "." + p;
if (mergeContext.getCachedMerge(cacheKey) != null) {
log.warn("Incoming change skipped %s:%s.%s, already processed", change.getClassName(), change.getUid(), p);
continue;
}
mergeContext.pushMerge(cacheKey, coll, false);
Map<String, Object> saved = mergeContext.getSavedProperties(dest);
boolean unsaved = mergeContext.isUnsaved(dest);
Object receivedEntity;
if (coll instanceof List<?>) {
List<Object> mergedColl = null;
receivedEntity = lookupEntity(mergeContext, val, dest, null);
// Check if we can find the complete initialized list in the incoming changes and use it instead of incremental updates
if (receivedEntity != null && mergeContext.getDataManager().getPropertyValue(receivedEntity, p) instanceof PersistentCollection
&& ((PersistentCollection<?>)mergeContext.getDataManager().getPropertyValue(receivedEntity, p)).wasInitialized())
mergedColl = (List<Object>)mergeContext.getDataManager().getPropertyValue(receivedEntity, p);
else {
Object target = coll instanceof PropertyHolder ? ((PropertyHolder)coll).getObject() : coll;
mergedColl = mergeContext.getDataManager().newInstance(target, List.class);
if (!unsaved)
mergedColl.addAll((List<?>)coll);
applyListChanges(mergeContext, mergedColl, (CollectionChanges)val, saved != null && saved.get(p) instanceof List<?> ? (List<Object>)saved.get(p) : null);
}
mergedChanges.put(p, mergedColl);
}
else if (coll instanceof Set<?>) {
Set<Object> mergedColl = null;
receivedEntity = lookupEntity(mergeContext, val, dest, null);
// Check if we can find the complete initialized list in the incoming changes and use it instead of incremental updates
if (receivedEntity != null && mergeContext.getDataManager().getPropertyValue(receivedEntity, p) instanceof PersistentCollection
&& ((PersistentCollection<?>)mergeContext.getDataManager().getPropertyValue(receivedEntity, p)).wasInitialized())
mergedColl = (Set<Object>)mergeContext.getDataManager().getPropertyValue(receivedEntity, p);
else {
Object target = coll instanceof PropertyHolder ? ((PropertyHolder)coll).getObject() : coll;
mergedColl = mergeContext.getDataManager().newInstance(target, Set.class);
if (!unsaved)
mergedColl.addAll((Set<?>)coll);
applySetChanges(mergeContext, mergedColl, (CollectionChanges)val, saved != null && saved.get(p) instanceof List<?> ? (List<Object>)saved.get(p) : null);
}
mergedChanges.put(p, mergedColl);
}
else if (coll instanceof Map<?, ?>) {
Map<Object, Object> mergedMap = null;
receivedEntity = lookupEntity(mergeContext, val, dest, null);
// Check if we can find the complete initialized map in the incoming changes and use it instead of incremental updates
if (receivedEntity != null && mergeContext.getDataManager().getPropertyValue(receivedEntity, p) instanceof PersistentCollection
&& ((PersistentCollection<?>)mergeContext.getDataManager().getPropertyValue(receivedEntity, p)).wasInitialized())
mergedMap = (Map<Object, Object>)mergeContext.getDataManager().getPropertyValue(receivedEntity, p);
else {
Object target = coll instanceof PropertyHolder ? ((PropertyHolder)coll).getObject() : coll;
mergedMap = mergeContext.getDataManager().newInstance(target, Map.class);
if (!unsaved)
mergedMap.putAll((Map<?, ?>)coll);
applyMapChanges(mergeContext, mergedMap, (CollectionChanges)val, saved != null && saved.get(p) instanceof List<?> ? (List<Object[]>)saved.get(p) : null);
}
mergedChanges.put(p, mergedMap);
}
}
else
mergedChanges.put(p, val);
}
Class<?> changeClass = TypeUtil.forName(changeEntityRef.getClassName());
Number version = change.getVersion();
// If dest version is greater than received change, use it instead
// That means that the received Change change is probably inconsistent with its content
if (incomingEntity != null && mergeContext.getDataManager().getVersion(incomingEntity) != null && (version == null || ((Number)mergeContext.getDataManager().getVersion(incomingEntity)).longValue() > version.longValue()))
version = (Number)mergeContext.getDataManager().getVersion(incomingEntity);
ChangeProxy changeProxy = new ChangeProxy(mergeContext.getDataManager().getUidPropertyName(changeClass),
change.getUid(), mergeContext.getDataManager().getIdPropertyName(changeClass),
change.getId(), mergeContext.getDataManager().getVersionPropertyName(changeClass), version, mergedChanges, templateObject);
// Merge the proxy (only actual changes will be merged, values not in mergedChanges will be ignored)
mergeContext.mergeExternal(changeProxy, dest, parent, propertyName);
// Ensure updated collections/maps will be processed only once
// Mark them in the current merge cache
for (String p : mergedChanges.keySet()) {
Object v = mergeContext.getDataManager().getPropertyValue(dest, p);
if (v instanceof Collection<?> || v instanceof Map<?, ?>)
mergeContext.pushMerge(v, v, false);
}
}
catch (ClassNotFoundException e) {
throw new RuntimeException("Received Change for unknown class", e);
}
catch (IllegalAccessException e) {
throw new RuntimeException("Error instantiating class", e);
}
catch (InstantiationException e) {
throw new RuntimeException("Error instantiating class", e);
}
finally {
mergeContext.setUninitializeAllowed(saveUninitAllowed);
}
if (dest != null)
log.debug("merge change result: %s", ObjectUtil.toString(dest));
}
return next;
}
private void applyListChanges(MergeContext mergeContext, List<Object> coll, CollectionChanges ccs, List<Object> savedArray) {
if (savedArray != null) {
// If the List has been modified locally, apply received operations to the current saved snapshot
List<Object> savedList = new ArrayList<Object>(savedArray);
for (CollectionChange cc : ccs.getChanges()) {
if (cc.getType() == -1) {
if (cc.getKey() != null && (Integer)cc.getKey() >= 0 && cc.getValue() instanceof ChangeRef
&& isForEntity(mergeContext, (ChangeRef) cc.getValue(), savedList.get((Integer)cc.getKey())))
savedList.remove(((Integer)cc.getKey()).intValue());
else if (cc.getKey() != null && (Integer)cc.getKey() >= 0 && mergeContext.objectEquals(cc.getValue(), savedList.get((Integer)cc.getKey())))
savedList.remove(((Integer)cc.getKey()).intValue());
else if (cc.getKey() == null && cc.getValue() instanceof ChangeRef) {
for (int i = 0; i < savedList.size(); i++) {
if (isForEntity(mergeContext, (ChangeRef)cc.getValue(), savedList.get(i))) {
savedList.remove(i);
i--;
}
}
}
else if (cc.getKey() == null) {
for (int i = 0; i < savedList.size(); i++) {
if (mergeContext.objectEquals(cc.getValue(), savedList.get(i))) {
savedList.remove(i);
i--;
}
}
}
}
else if (cc.getType() == 1) {
if (cc.getKey() != null && (Integer)cc.getKey() >= 0) {
int key = (Integer)cc.getKey();
if (key > savedList.size())
log.warn("Could not add received element at index %d", key);
else
savedList.add(key, cc.getValue());
}
else if (cc.getKey() != null)
savedList.add(cc.getValue());
else
savedList.add(cc.getValue());
}
else if (cc.getType() == 0 && cc.getKey() != null && (Integer)cc.getKey() >= 0) {
savedList.set(((Integer)cc.getKey()).intValue(), cc.getValue());
}
}
// Replace local objects by received objects in merged collection
for (int i = 0; i < coll.size(); i++) {
for (Object e : savedList) {
if (mergeContext.objectEquals(coll.get(i), e) && coll.get(i) != e) {
coll.set(i, e);
break;
}
}
}
savedArray.clear();
savedArray.addAll(savedList);
}
else {
// If the List has not been modified locally, apply received operations to the current collection content
for (CollectionChange cc : ccs.getChanges()) {
if (cc.getType() == -1) {
if (cc.getKey() != null && (Integer)cc.getKey() >= 0 && cc.getValue() instanceof ChangeRef
&& isForEntity(mergeContext, (ChangeRef)cc.getValue(), coll.get((Integer)cc.getKey())))
coll.remove(((Integer)cc.getKey()).intValue());
else if (cc.getKey() != null && (Integer)cc.getKey() >= 0 && mergeContext.objectEquals(cc.getValue(), coll.get((Integer)cc.getKey())))
coll.remove(((Integer)cc.getKey()).intValue());
else if (cc.getKey() == null && cc.getValue() instanceof ChangeRef) {
for (int i = 0; i < coll.size(); i++) {
if (isForEntity(mergeContext, (ChangeRef) cc.getValue(), coll.get(i))) {
coll.remove(i);
i--;
}
}
}
else if (cc.getKey() == null) {
for (int i = 0; i < coll.size(); i++) {
if (isForEntity(mergeContext, (ChangeRef) cc.getValue(), coll.get(i))) {
coll.remove(i);
i--;
}
}
}
}
else if (cc.getType() == 1) {
if (cc.getKey() != null && (Integer)cc.getKey() >= 0) {
int key = (Integer)cc.getKey();
if (key > coll.size())
log.warn("Could not add received element at index %d", key);
else
coll.add(key, cc.getValue());
}
else if (cc.getKey() != null)
coll.add(cc.getValue());
else
coll.add(cc.getValue());
}
else if (cc.getType() == 0 && cc.getKey() != null && (Integer)cc.getKey() >= 0) {
coll.set((Integer) cc.getKey(), cc.getValue());
}
}
}
}
private void applySetChanges(MergeContext mergeContext, Set<Object> coll, CollectionChanges ccs, List<Object> savedArray) {
if (savedArray != null) {
// If the Set has been modified locally, apply received operations to the current saved snapshot
List<Object> savedList = new ArrayList<Object>(savedArray);
for (CollectionChange cc : ccs.getChanges()) {
if (cc.getType() == -1) {
if (cc.getValue() instanceof ChangeRef) {
for (int i = 0; i < savedList.size(); i++) {
if (isForEntity(mergeContext, (ChangeRef)cc.getValue(), savedList.get(i))) {
savedList.remove(i);
i--;
}
}
}
else {
for (int i = 0; i < savedList.size(); i++) {
if (mergeContext.objectEquals(cc.getValue(), savedList.get(i))) {
savedList.remove(i);
i--;
}
}
}
}
else if (cc.getType() == 1) {
savedList.add(cc.getValue());
}
}
// Replace local objects by received objects in merged collection
List<Object> toAdd = new ArrayList<Object>();
for (Iterator<Object> ic = coll.iterator(); ic.hasNext(); ) {
Object c = ic.next();
for (Object e : savedList) {
if (mergeContext.objectEquals(c, e) && c != e) {
ic.remove();
toAdd.add(e);
break;
}
}
}
coll.addAll(toAdd);
savedArray.clear();
savedArray.addAll(savedList);
}
else {
// If the Set has not been modified locally, apply received operations to the current collection content
for (CollectionChange cc : ccs.getChanges()) {
if (cc.getType() == -1) {
if (cc.getKey() == null && cc.getValue() instanceof ChangeRef) {
for (Iterator<Object> ic = coll.iterator(); ic.hasNext(); ) {
Object c = ic.next();
if (isForEntity(mergeContext, (ChangeRef) cc.getValue(), c))
ic.remove();
}
}
else if (cc.getKey() == null) {
for (Iterator<Object> ic = coll.iterator(); ic.hasNext(); ) {
Object c = ic.next();
if (isForEntity(mergeContext, (ChangeRef) cc.getValue(), c))
ic.remove();
}
}
}
else if (cc.getType() == 1) {
coll.add(cc.getValue());
}
}
}
}
private void applyMapChanges(MergeContext mergeContext, Map<Object, Object> map, CollectionChanges ccs, List<Object[]> savedArray) {
if (savedArray != null) {
// If map has been modified locally, apply received operations to the current saved snapshot
Map<Object, Object> savedMap = new HashMap<Object, Object>();
for (Object[] se : savedArray)
savedMap.put(se[0], se[1]);
for (CollectionChange cc : ccs.getChanges()) {
Object key = cc.getKey() instanceof ChangeRef ? mergeContext.getCachedObject(cc.getKey()) : cc.getKey();
if (cc.getType() == -1) {
if (key != null && cc.getValue() instanceof ChangeRef && isForEntity(mergeContext, (ChangeRef)cc.getValue(), savedMap.get(key)))
savedMap.remove(key);
else if (key != null && mergeContext.objectEquals(cc.getValue(), savedMap.get(key)))
savedMap.remove(key);
}
else if (cc.getType() == 0 || cc.getType() == 1) {
savedMap.put(key, cc.getValue());
}
}
// Replace local objects by received objects in merged map
Map<Object, Object> toAdd = new HashMap<Object, Object>();
for (Iterator<Map.Entry<Object, Object>> ime = map.entrySet().iterator(); ime.hasNext(); ) {
Map.Entry<Object, Object> me = ime.next();
Object key = me.getKey();
Object value = me.getValue();
for (Object k : savedMap.keySet()) {
if (mergeContext.objectEquals(key, k) && key != k) {
ime.remove();
key = k;
toAdd.put(key, value);
}
if (mergeContext.objectEquals(value, k) && value != k) {
value = k;
toAdd.put(key, value);
}
Object v = savedMap.get(k);
if (mergeContext.objectEquals(key, v) && key != v) {
ime.remove();
key = v;
toAdd.put(key, value);
}
if (mergeContext.objectEquals(value, v) && value != v) {
value = v;
toAdd.put(key, value);
}
}
}
map.putAll(toAdd);
savedArray.clear();
for (Map.Entry<Object, Object> me : savedMap.entrySet())
savedArray.add(new Object[] { me.getKey(), me.getValue() });
}
else {
// Map has not been modified, just apply received operations to current content
for (CollectionChange cc : ccs.getChanges()) {
Object key = cc.getKey() instanceof ChangeRef ? mergeContext.getCachedObject(cc.getKey()) : cc.getKey();
if (cc.getType() == -1) {
if (key != null && cc.getValue() instanceof ChangeRef && isForEntity(mergeContext, (ChangeRef)cc.getValue(), map.get(key)))
map.remove(key);
else if (key != null && mergeContext.objectEquals(cc.getValue(), map.get(key)))
map.remove(key);
}
else if (cc.getType() == 0 || cc.getType() == 1) {
// Not found in local changes, apply remote change
map.put(key, cc.getValue());
}
}
}
}
private Object lookupEntity(MergeContext mergeContext, Object graph, Object obj, IdentityHashMap<Object, Boolean> cache) {
if (graph == null)
return null;
if (!(graph.getClass().isArray()) && (ObjectUtil.isSimple(graph) || graph instanceof Value || graph instanceof byte[] || graph instanceof Enum))
return null;
if (cache == null)
cache = new IdentityHashMap<Object, Boolean>();
if (cache.containsKey(graph))
return null;
cache.put(graph, true);
if (mergeContext.getDataManager().isEntity(graph) && !mergeContext.getDataManager().isInitialized(graph))
return null;
if (mergeContext.objectEquals(graph, obj) && graph != obj)
return graph;
Object found = null;
if (graph instanceof CollectionChanges) {
for (CollectionChange cc : ((CollectionChanges)graph).getChanges()) {
found = lookupEntity(mergeContext, cc, obj, cache);
if (found != null)
return found;
}
}
else if (graph instanceof CollectionChange) {
if (((CollectionChange)graph).getKey() != null) {
found = lookupEntity(mergeContext, ((CollectionChange)graph).getKey(), obj, cache);
if (found != null)
return found;
}
if (((CollectionChange)graph).getValue() != null) {
found = lookupEntity(mergeContext, ((CollectionChange)graph).getValue(), obj, cache);
if (found != null)
return found;
}
return null;
}
if (graph instanceof PersistentCollection && !((PersistentCollection<?>)graph).wasInitialized())
return null;
if (graph.getClass().isArray()) {
for (int i = 0; i < Array.getLength(graph); i++) {
found = lookupEntity(mergeContext, Array.get(graph, i), obj, cache);
if (found != null)
return found;
}
}
else if (graph instanceof Collection<?>) {
for (Object elt : ((Collection<?>)graph)) {
found = lookupEntity(mergeContext, elt, obj, cache);
if (found != null)
return found;
}
return null;
}
else if (graph instanceof Map<?, ?>) {
for (Map.Entry<?, ?> me : ((Map<?, ?>)graph).entrySet()) {
found = lookupEntity(mergeContext, me.getKey(), obj, cache);
if (found != null)
return found;
found = lookupEntity(mergeContext, me.getValue(), obj, cache);
if (found != null)
return found;
}
return null;
}
else {
for (Object v : mergeContext.getDataManager().getPropertyValues(graph, true, false, true).values()) {
found = lookupEntity(mergeContext, v, obj, cache);
if (found != null)
return found;
}
return null;
}
return null;
}
}