/**
* GRANITE DATA SERVICES
* Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S.
*
* This file is part of the Granite Data Services Platform.
*
* Granite Data Services is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 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 Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA, or see <http://www.gnu.org/licenses/>.
*/
package org.granite.tide.data;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
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 org.granite.config.ConvertersConfig;
import org.granite.context.GraniteContext;
import org.granite.logging.Logger;
import org.granite.messaging.amf.io.convert.Converters;
import org.granite.messaging.amf.io.util.ClassGetter;
import org.granite.messaging.service.ServiceException;
import org.granite.util.Entity;
import org.granite.util.Introspector;
import org.granite.util.PropertyDescriptor;
import org.granite.util.TypeUtil;
/**
* Utility class that applies a ChangeSet on a persistence context
* @author William DRAI
*
*/
public class ChangeSetApplier {
private static final Logger log = Logger.getLogger(ChangeSetApplier.class);
private TidePersistenceAdapter persistenceAdapter;
public ChangeSetApplier(TidePersistenceAdapter persistenceAdapter) {
this.persistenceAdapter = persistenceAdapter;
}
protected long getVersion(Entity e) {
if (!e.isVersioned())
throw new IllegalStateException("Cannot apply change set on non versioned entity " + e.getName());
Number version = (Number)e.getVersion();
if (version == null)
throw new IllegalStateException("Cannot apply changes on non persistent entity " + e.getName() + ":" + e.getIdentifier());
return version.longValue();
}
protected Object mergeObject(Object entity, IdentityHashMap<Object, Object> cache) {
if (entity == null)
return null;
ConvertersConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
ClassGetter classGetter = config.getClassGetter();
Converters converters = config.getConverters();
if (entity != null && !classGetter.isInitialized(null, null, entity)) {
// cache.contains() cannot be called on an unintialized proxy because hashCode will fail !!
Class<?> cls = classGetter.getClass(entity);
Entity e = new Entity(cls);
return persistenceAdapter.find(cls, (Serializable)converters.convert(classGetter.getIdentifier(entity), e.getIdentifierType()));
}
if (cache.containsKey(entity))
return cache.get(entity);
if (entity instanceof ChangeRef) {
ChangeRef ref = (ChangeRef)entity;
try {
Class<?> entityClass = TypeUtil.forName(ref.getClassName());
Entity e = new Entity(entityClass);
Serializable refId = (Serializable)converters.convert(ref.getId(), e.getIdentifierType());
// Lookup in current merge cache, if not found then lookup from persistence adapter
for (Object cached : cache.values()) {
if (cached.getClass().equals(entityClass) && refId.equals(e.getIdentifier(cached)))
return cached;
}
Object managedEntity = persistenceAdapter.find(entityClass, refId);
cache.put(entity, managedEntity);
return managedEntity;
}
catch (ClassNotFoundException cnfe) {
throw new ServiceException("Could not find class " + ref.getClassName(), cnfe);
}
}
cache.put(entity, entity);
if (entity != null && classGetter.isEntity(entity) && classGetter.isInitialized(null, null, entity)) {
// If the entity has an id, we should get the managed instance
Entity e = new Entity(entity);
Object id = e.getIdentifier();
if (id != null) {
Object managedEntity = persistenceAdapter.find(entity.getClass(), (Serializable)id);
cache.put(entity, managedEntity);
return managedEntity;
}
cache.put(entity, entity);
// If there is no id, traverse the object graph to merge associated objects
List<Object[]> fieldValues = classGetter.getFieldValues(entity);
for (Object[] fieldValue : fieldValues) {
Object value = fieldValue[1];
Field field = (Field)fieldValue[0];
if (value == null)
continue;
if (!classGetter.isInitialized(entity, field.getName(), value)) {
if (!classGetter.getClass(value).isAnnotationPresent(javax.persistence.Entity.class))
continue;
try {
// Lookup in cache
Serializable valueId = classGetter.getIdentifier(value);
Object newValue = null;
Entity ve = new Entity(field.getType());
for (Object cached : cache.values()) {
if (field.getType().isInstance(cached) && valueId.equals(ve.getIdentifier(cached))) {
newValue = cached;
break;
}
}
if (newValue == null)
newValue = persistenceAdapter.find(field.getType(), valueId);
field.set(entity, newValue);
}
catch (IllegalAccessException e1) {
throw new ServiceException("Could not set entity field value on " + value.getClass() + "." + field.getName());
}
}
if (value instanceof Collection<?>) {
@SuppressWarnings("unchecked")
Collection<Object> coll = (Collection<Object>)value;
Iterator<Object> icoll = coll.iterator();
Set<Object> addedElements = new HashSet<Object>();
int idx = 0;
while (icoll.hasNext()) {
Object element = icoll.next();
if (element != null) {
Object newElement = mergeObject(element, cache);
if (newElement != element) {
if (coll instanceof List<?>)
((List<Object>)coll).set(idx, newElement);
else {
icoll.remove();
addedElements.add(newElement);
}
}
}
idx++;
}
if (!(coll instanceof List<?>))
coll.addAll(addedElements);
}
else if (value.getClass().isArray()) {
for (int idx = 0; idx < Array.getLength(value); idx++) {
Object element = Array.get(value, idx);
if (element == null)
continue;
Object newElement = mergeObject(element, cache);
if (newElement != element)
Array.set(value, idx, newElement);
}
}
else if (value instanceof Map<?, ?>) {
@SuppressWarnings("unchecked")
Map<Object, Object> map = (Map<Object, Object>)value;
Iterator<Entry<Object, Object>> ime = map.entrySet().iterator();
Map<Object, Object> addedElements = new HashMap<Object, Object>();
while (ime.hasNext()) {
Entry<Object, Object> me = ime.next();
Object val = me.getValue();
if (val != null) {
Object newVal = mergeObject(val, cache);
if (newVal != val)
me.setValue(newVal);
}
Object key = me.getKey();
if (key != null) {
Object newKey = mergeObject(key, cache);
if (newKey != key) {
ime.remove();
addedElements.put(newKey, me.getValue());
}
}
}
map.putAll(addedElements);
}
else if (classGetter.isEntity(value)) {
Object newValue = mergeObject(value, cache);
if (newValue != value) {
try {
field.set(entity, newValue);
}
catch (IllegalAccessException e1) {
throw new ServiceException("Could not set entity field value on " + value.getClass() + "." + field.getName());
}
}
}
}
}
return entity;
}
public Object[] applyChanges(ChangeSet changeSet) {
IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
Object[] appliedChanges = new Object[changeSet.getChanges().length];
for (int i = 0; i < changeSet.getChanges().length; i++)
appliedChanges[i] = applyChange(changeSet.getChanges()[i], cache);
return appliedChanges;
}
@SuppressWarnings("unchecked")
private Object applyChange(Change change, IdentityHashMap<Object, Object> cache) {
Converters converters = ((ConvertersConfig)GraniteContext.getCurrentInstance().getGraniteConfig()).getConverters();
Object appliedChange = null;
try {
Class<?> entityClass = TypeUtil.forName(change.getClassName());
if (change.getId() != null) {
Entity e = new Entity(entityClass);
Type identifierType = e.getIdentifierType();
Object entity = persistenceAdapter.find(entityClass, (Serializable)converters.convert(change.getId(), identifierType));
if (entity == null) {
log.debug("Entity not found, maybe has already been deleted by cascading");
return null;
}
e = new Entity(entity);
Long version = getVersion(e);
if ((change.getVersion() != null && change.getVersion().longValue() < version) || (change.getVersion() == null && version != null))
persistenceAdapter.throwOptimisticLockException(entity);
appliedChange = entity;
for (Entry<String, Object> me : change.getChanges().entrySet()) {
try {
PropertyDescriptor[] propertyDescriptors = Introspector.getPropertyDescriptors(entityClass);
PropertyDescriptor propertyDescriptor = null;
for (PropertyDescriptor pd : propertyDescriptors) {
if (pd.getName().equals(me.getKey())) {
propertyDescriptor = pd;
break;
}
}
if (propertyDescriptor == null) {
log.warn("Could not find property " + me.getKey() + " on class " + change.getClassName());
}
else {
if (me.getValue() instanceof CollectionChanges) {
Object collection = propertyDescriptor.getReadMethod().invoke(entity);
CollectionChanges collectionChanges = (CollectionChanges)me.getValue();
for (CollectionChange collectionChange : collectionChanges.getChanges()) {
if (collectionChange.getType() == 1) {
if (collection instanceof Set<?>) {
Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
Type elementType = Object.class;
if (collectionType instanceof ParameterizedType)
elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
((Set<Object>)collection).add(value);
}
else if (collection instanceof List<?>) {
Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
Type elementType = Object.class;
if (collectionType instanceof ParameterizedType)
elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
((List<Object>)collection).add((Integer)collectionChange.getKey(), value);
}
else if (collection instanceof Map<?, ?>) {
Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
Type keyType = Object.class;
Type valueType = Object.class;
if (mapType instanceof ParameterizedType) {
keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
valueType = ((ParameterizedType)mapType).getActualTypeArguments()[1];
}
Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), valueType);
((Map<Object, Object>)collection).put(key, value);
}
}
else if (collectionChange.getType() == -1) {
if (collection instanceof Set<?>) {
Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
Type elementType = Object.class;
if (collectionType instanceof ParameterizedType)
elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
Object removed = ((Set<Object>)collection).remove(value);
cache.put(removed, value);
}
else if (collection instanceof List<?>) {
int index = (Integer)collectionChange.getKey();
Object removed = ((List<Object>)collection).remove(index);
cache.put(removed, removed);
}
else if (collection instanceof Map<?, ?>) {
Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
Type keyType = Object.class;
if (mapType instanceof ParameterizedType)
keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
Object removed = ((Map<Object, Object>)collection).remove(key);
cache.put(removed, key);
}
}
else if (collectionChange.getType() == 0) {
if (collection instanceof Set<?>) {
// This should not happen with a Set !!
throw new IllegalStateException("Cannot replace an indexed element on a Set, don't use setItemAt on an ArrayCollection representing a Set !");
}
else if (collection instanceof List<?>) {
int index = (Integer)collectionChange.getKey();
Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
Type elementType = Object.class;
if (collectionType instanceof ParameterizedType)
elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
((List<Object>)collection).set(index, value);
}
else if (collection instanceof Map<?, ?>) {
Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
Type keyType = Object.class;
Type valueType = Object.class;
if (mapType instanceof ParameterizedType) {
keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
valueType = ((ParameterizedType)mapType).getActualTypeArguments()[1];
}
Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), valueType);
((Map<Object, Object>)collection).put(key, value);
}
}
}
}
else {
if (propertyDescriptor.getWriteMethod() != null) {
Object value = mergeObject(me.getValue(), cache);
value = converters.convert(value, propertyDescriptor.getWriteMethod().getGenericParameterTypes()[0]);
propertyDescriptor.getWriteMethod().invoke(entity, value);
}
else
log.warn("Property " + me.getKey() + " on class " + change.getClassName() + " is not writeable");
}
}
}
catch (InvocationTargetException ite) {
throw new ServiceException("Could not set property " + me.getKey(), ite.getTargetException());
}
catch (IllegalAccessException iae) {
throw new ServiceException("Could not set property " + me.getKey(), iae);
}
}
}
return appliedChange;
}
catch (ClassNotFoundException cnfe) {
throw new ServiceException("Could not find class " + change.getClassName(), cnfe);
}
}
}