/**
* 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.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import org.granite.client.messaging.ClientAliasRegistry;
import org.granite.client.tide.data.impl.ObjectUtil;
import org.granite.client.tide.data.spi.DataManager;
import org.granite.client.tide.server.ServerSession;
import org.granite.tide.IUID;
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;
/**
* ChangeSetBuilder is a utility class that builds a ChangeSet for an existing dirty context
* or a particular entity instance
*
* @author William DRAI
*/
public class ChangeSetBuilder {
private final EntityManager entityManager;
private final DataManager dataManager;
private final ServerSession serverSession;
private final ClientAliasRegistry aliasRegistry;
private final Map<Object, Map<String, Object>> savedProperties;
private final boolean local;
private EntityManager tmpEntityManager = null;
public ChangeSetBuilder(EntityManager entityManager, ServerSession serverSession) {
this(entityManager, serverSession, true);
}
public ChangeSetBuilder(EntityManager entityManager, ServerSession serverSession, boolean local) {
this.entityManager = entityManager;
this.dataManager = entityManager.getDataManager();
this.serverSession = serverSession;
this.aliasRegistry = serverSession.getAliasRegistry();
this.savedProperties = entityManager.getSavedProperties();
this.local = local;
// Temporary context to store complete entities so we can uninitialize all collections
// when possible
tmpEntityManager = entityManager.newTemporaryEntityManager();
}
/**
* Build a ChangeSet object for the current context
*
* @return the change set for this context
*/
public ChangeSet buildChangeSet() {
return internalBuildChangeSet(savedProperties);
}
/**
* Build a Change object for the entity in the current context
*
* @return the change for this entity in the context
*/
public ChangeSet buildEntityChangeSet(Object entity) {
Map<Object, Map<String, Object>> entitySavedProperties = new IdentityHashMap<Object, Map<String, Object>>();
collectEntitySavedProperties(entity, entitySavedProperties, null);
if (!entitySavedProperties.containsKey(entity)) {
Map<String, Object> sp = new HashMap<String, Object>();
sp.put(dataManager.getVersionPropertyName(entity), dataManager.getVersion(entity));
entitySavedProperties.put(entity, sp);
}
ChangeSet changeSet = internalBuildChangeSet(entitySavedProperties);
// Place Change for initial entity first
int pos = 0;
for (int i = 0; i < changeSet.getChanges().length; i++) {
if (isForEntity(changeSet.getChanges()[i], entity)) {
pos = i;
break;
}
}
if (pos > 0) {
Change change = changeSet.getChanges()[pos];
System.arraycopy(changeSet.getChanges(), 0, changeSet.getChanges(), 1, pos);
changeSet.getChanges()[0] = change;
}
return changeSet;
}
private String getType(String alias) {
String className = aliasRegistry.getTypeForAlias(alias);
return className != null ? className : alias;
}
private String getAlias(String className) {
String alias = aliasRegistry.getAliasForType(className);
return alias != null ? alias : className;
}
private boolean isForEntity(Change change, Object entity) {
return entity.getClass().getName().equals(getType(change.getClassName())) && change.getUid().equals(dataManager.getUid(entity));
}
private boolean isEntity(Object entity) {
return entityManager.getDataManager().isEntity(entity);
}
private void collectEntitySavedProperties(Object entity, Map<Object, Map<String, Object>> savedProperties, Map<Object, Boolean> cache) {
if (cache == null)
cache = new IdentityHashMap<Object, Boolean>();
else if (cache.containsKey(entity))
return;
cache.put(entity, true);
if (isEntity(entity) && this.savedProperties.containsKey(entity))
savedProperties.put(entity, this.savedProperties.get(entity));
Map<String, Object> properties = dataManager.getPropertyValues(entity, false, false);
for (Object v : properties.values()) {
if (v == null)
continue;
if (isEntity(entity) && !dataManager.isInitialized(v))
continue;
if (v instanceof Collection<?>) {
for (Object e : ((Collection<?>)v))
collectEntitySavedProperties(e, savedProperties, cache);
}
else if (v instanceof Map<?, ?>) {
for (Map.Entry<?, ?> me : ((Map<?, ?>)v).entrySet()) {
collectEntitySavedProperties(me.getKey(), savedProperties, cache);
collectEntitySavedProperties(me.getValue(), savedProperties, cache);
}
}
else if (!ObjectUtil.isSimple(v) && !(v instanceof Enum || v instanceof Value || v instanceof byte[])) {
collectEntitySavedProperties(v, savedProperties, cache);
}
}
}
@SuppressWarnings("unchecked")
private ChangeSet internalBuildChangeSet(Map<Object, Map<String, Object>> savedProperties) {
ChangeSet changeSet = new ChangeSet(new Change[0], local);
Map<Object, Object> changeMap = new IdentityHashMap<Object, Object>();
for (Map.Entry<Object, Map<String, Object>> mes : savedProperties.entrySet()) {
Object entity = mes.getKey();
// if (savedProperties[entity] is Array)
// entity = _context.meta_getOwnerEntity(entity);
if (!isEntity(entity) && !(entity instanceof IUID))
entity = entityManager.getOwnerEntity(entity);
if (changeMap.containsKey(entity))
continue;
Map<String, Object> save = mes.getValue();
Change change = null;
String versionPropertyName = null;
if (isEntity(entity)) {
versionPropertyName = dataManager.getVersionPropertyName(entity);
// Unsaved objects should not be part of the ChangeSet
if (save.get(versionPropertyName) == null)
continue;
change = new Change(getAlias(entity.getClass().getName()),
dataManager.hasIdProperty(entity) ? (Serializable)dataManager.getId(entity) : null,
(Number)dataManager.getVersion(entity), dataManager.getUid(entity),
local);
}
else if (entity instanceof IUID) {
change = new Change(getAlias(entity.getClass().getName()), null, null, dataManager.getUid(entity), local);
}
if (change == null) {
changeMap.put(entity, false);
continue;
}
changeMap.put(entity, change);
Change[] changes = new Change[changeSet.getChanges().length+1];
System.arraycopy(changeSet.getChanges(), 0, changes, 0, changeSet.getChanges().length);
changes[changeSet.getChanges().length] = change;
changeSet.setChanges(changes);
for (Map.Entry<String, Object> me : dataManager.getPropertyValues(entity, true, true, false ).entrySet()) {
String p = me.getKey();
Object v = me.getValue();
if (save != null && save.containsKey(p)) {
if (v instanceof Collection<?> || v instanceof Map<?, ?>) {
List<?> collSnapshot = (List<?>)save.get(p);
CollectionChanges collChanges = new CollectionChanges();
change.getChanges().put(p, collChanges);
List<Object[]> diffOps = null;
if (v instanceof Map<?, ?>)
diffOps = Utils.diffMaps(Utils.convertMapSnapshot((List<Object[]>)collSnapshot), (Map<?, ?>)v);
else if (v instanceof List<?>)
diffOps = Utils.diffLists(collSnapshot, (List<?>)v);
else
diffOps = Utils.diffColls(collSnapshot, (Collection<?>)v);
CollectionChange[] collectionChanges = new CollectionChange[diffOps.size()];
int idx = 0;
for (Object[] op : diffOps)
collectionChanges[idx++] = new CollectionChange((Integer)op[0], buildRef(op[1]), buildRef(op[2]));
collChanges.setChanges(collectionChanges);
}
else {
change.getChanges().put(p, buildRef(v));
}
}
else if (savedProperties.containsKey(v) && !isEntity(v) && !(v instanceof IUID)) {
// Embedded objects
change.getChanges().put(p, v);
changeMap.put(v, false);
}
}
}
// Cleanup tmp context to detach all new entities
tmpEntityManager.clear();
return changeSet;
}
private Object buildRef(Object object) {
if (!isEntity(object))
return object;
if (!dataManager.hasVersionProperty(object))
throw new IllegalArgumentException("Cannot build ChangeSet for non versioned entity " + object.getClass().getName());
if (dataManager.getVersion(object) != null)
return new ChangeRef(getAlias(object.getClass().getName()), dataManager.getUid(object), (Serializable)dataManager.getId(object));
// Force attachment/init of uids of ref object in case some deep elements in the graph are not yet managed in the current context
// So the next merge in the tmp context does not attach newly added objects to the tmp context
entityManager.attach(object);
return tmpEntityManager.mergeFromEntityManager(entityManager, serverSession, object, null, true);
}
}