package org.springmodules.prevayler.system;
import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.log4j.Logger;
import org.springmodules.prevayler.support.PrevaylerCascadePersistenceException;
import org.springmodules.prevayler.support.PrevaylerConfigurationException;
import org.springmodules.prevayler.support.PrevaylerDataRetrievalException;
import org.springmodules.prevayler.support.PrevaylerUnsavedObjectException;
import org.springmodules.prevayler.system.callback.SystemCallback;
import org.springmodules.prevayler.id.DefaultIdMerger;
import org.springmodules.prevayler.id.IdMerger;
/**
* <p>{@link PrevalentSystem} implementation based on concurrent hash maps.</p>
* <p>The only mandatory property to set here is the {@link PrevalenceInfo}.</p>
* <p>This class is <b>thread safe</b>.</p>
*
* @author Sergio Bossa
*/
public class DefaultPrevalentSystem implements PrevalentSystem {
private static final long serialVersionUID = 476105268506333743L;
private transient static final Logger logger = Logger.getLogger(DefaultPrevalentSystem.class);
private transient static final ThreadLocal localIdentityMap = new ThreadLocal();
// FIXME: Currently not configurable:
private IdMerger merger = new DefaultIdMerger();
private PrevalenceInfo prevalenceInfo = new PrevalenceInfo();
private Map entitiesGlobalMap = new ConcurrentHashMap();
public DefaultPrevalentSystem() {
this.merger.setPrevalenceInfo(this.prevalenceInfo);
}
public Object save(Object newEntity) {
this.localIdentityMap.set(new IdentityHashMap());
this.internalSave(newEntity);
return newEntity;
}
public Object update(Object entity) {
Object id = null;
try {
id = this.prevalenceInfo.getIdResolutionStrategy().resolveId(entity).get(entity);
if (id != null) {
this.localIdentityMap.set(new IdentityHashMap());
this.internalUpdate(entity, id);
return entity;
} else {
throw new PrevaylerUnsavedObjectException("Cannot update unsaved object!");
}
} catch (IllegalAccessException actual) {
throw new IllegalStateException("Cannot access id value: " + id, actual);
}
}
public void delete(Object entity) {
Object id = null;
try {
Map entityMap = this.lookupMap(entity.getClass());
id = this.prevalenceInfo.getIdResolutionStrategy().resolveId(entity).get(entity);
if (id != null) {
Object old = entityMap.get(id);
if (old != null) {
entityMap.remove(id);
} else {
throw new PrevaylerDataRetrievalException("Cannot find object with id: " + id);
}
} else {
throw new PrevaylerUnsavedObjectException("Cannot delete unsaved object!");
}
} catch (IllegalAccessException actual) {
throw new IllegalStateException("Cannot access id value: " + id, actual);
}
}
public void delete(Class entityClass) {
Map entityMap = this.lookupMap(entityClass);
entityMap.clear();
}
public Object get(Class entityClass, Object id) {
Map entityMap = this.lookupMap(entityClass);
return entityMap.get(id);
}
public List get(Class entityClass) {
Map entityMap = this.lookupMap(entityClass);
List result = new LinkedList();
Iterator it = entityMap.values().iterator();
while (it.hasNext()) {
Object currentEntity = it.next();
if (entityClass.isAssignableFrom(currentEntity.getClass())) {
result.add(currentEntity);
}
}
return result;
}
public void merge(Object sourceEntity, Object destinationEntity) {
this.merger.merge(sourceEntity, destinationEntity);
}
public Object execute(SystemCallback callback) {
return callback.execute(this);
}
public void setPrevalenceInfo(PrevalenceInfo info) {
this.prevalenceInfo = info;
this.merger.setPrevalenceInfo(this.prevalenceInfo);
}
/*** Persistence internals ***/
private Object internalSave(Object newEntity) {
DefaultPrevalentSystem.logger.debug("Saving object: " + newEntity);
Object id = null;
try {
// Get the identity map by entity class:
Map entityMap = this.lookupMap(newEntity.getClass());
// Assign id:
id = this.prevalenceInfo.getIdGenerationStrategy().generateId();
this.prevalenceInfo.getIdResolutionStrategy().resolveId(newEntity).set(newEntity, id);
// Add the entity to the thread local identity map, for tracking its traversal (an entity once traversed must not be tarversed again):
IdentityHashMap localIdentityMap = (IdentityHashMap) DefaultPrevalentSystem.localIdentityMap.get();
localIdentityMap.put(newEntity, new Integer(1));
// Add the new entity to the system:
entityMap.put(id, newEntity);
// Self cascade persistence (needed for updating pointers to already persisted entities):
this.doCascadePersistence(newEntity, newEntity);
// Return the new, updated and saved, entity:
return newEntity;
} catch(IllegalAccessException actual) {
throw new IllegalStateException("Cannot access id value: " + id, actual);
}
}
private Object internalUpdate(Object updatedEntity, Object id) {
DefaultPrevalentSystem.logger.debug("Updating object " + updatedEntity + " with id: " + id);
// Get the identity map by the entity class:
Map entityMap = this.lookupMap(updatedEntity.getClass());
// Look for the entity in the map and update it (if found):
Object currentEntity = entityMap.get(id);
if ((currentEntity != null) && (currentEntity.getClass().equals(updatedEntity.getClass()))) {
IdentityHashMap localIdentityMap = (IdentityHashMap) DefaultPrevalentSystem.localIdentityMap.get();
// If the object has still not been reached:
if (localIdentityMap.get(currentEntity) == null) {
// Add the actual entity to the thread local identity map, for tracking its traversal (an entity once traversed must not be tarversed again):
localIdentityMap.put(currentEntity, new Integer(1));
// Copy new into actual and do cascade persistence:
this.doCascadePersistence(updatedEntity, currentEntity);
}
// Return the updated (original) entity:
return currentEntity;
} else {
throw new PrevaylerDataRetrievalException("Cannot find object with id: " + id);
}
}
private void doCascadePersistence(Object source, Object destination) {
Class currentClass = source.getClass();
while (currentClass != null) {
try {
Field[] fields = currentClass.getDeclaredFields();
for (int counter = 0; counter < fields.length; counter++) {
// Get the field:
Field currentField = fields[counter];
currentField.setAccessible(true);
// Update the field, from source to destination:
this.updateValue(source, destination, currentField);
}
currentClass = currentClass.getSuperclass();
} catch(Exception ex) {
throw new PrevaylerCascadePersistenceException(ex.getMessage(), ex);
}
}
}
private void updateValue(Object source, Object destination, Field field) throws Exception {
int modifiers = field.getModifiers();
if (! Modifier.isFinal(modifiers) && ! Modifier.isStatic(modifiers)) {
Object value = field.get(source);
if (value != null) {
value = this.getUpdatedValue(value);
}
field.set(destination, value);
}
}
private Object getUpdatedValue(Object value) throws Exception {
Object result = value;
if (value.getClass().isArray()) {
result = this.getUpdatedArray((Object[]) value);
} else if (Collection.class.isAssignableFrom(value.getClass())) {
result = this.getUpdatedCollection((Collection) value);
} else if (Map.class.isAssignableFrom(value.getClass())) {
result = this.getUpdatedMap((Map) value);
} else if (this.prevalenceInfo.getPrevalentClass(value.getClass()) != null) {
result = this.getUpdatedPrevalentObject(value);
}
return result;
}
private Object getUpdatedArray(Object[] array) throws Exception {
Object[] result = (Object[]) array.clone();
for (int i = 0; i < array.length; i++) {
result[i] = this.getUpdatedValue(array[i]);
}
return result;
}
private Collection getUpdatedCollection(Collection collection) throws Exception {
Collection result = (Collection) collection.getClass().getConstructor(new Class[0]).newInstance(new Object[0]);
Iterator it = collection.iterator();
while (it.hasNext()) {
result.add(this.getUpdatedValue(it.next()));
}
return result;
}
private Map getUpdatedMap(Map map) throws Exception {
Map result = (Map) map.getClass().getConstructor(new Class[0]).newInstance(new Object[0]);
Iterator it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Entry) it.next();
Object key = this.getUpdatedValue(entry.getKey());
Object value = this.getUpdatedValue(entry.getValue());
result.put(key, value);
}
return result;
}
private Object getUpdatedPrevalentObject(Object entity) {
Object result = null;
Object id = null;
try {
Field idField = this.prevalenceInfo.getIdResolutionStrategy().resolveId(entity);
id = idField.get(entity);
if (id == null) {
result = this.internalSave(entity);
} else {
result = this.internalUpdate(entity, id);
}
return result;
} catch (IllegalAccessException actual) {
throw new IllegalStateException("Cannot access id value: " + id, actual);
}
}
/** Other internals **/
private Map lookupMap(Class entityClass) {
Class prevalentClass = this.prevalenceInfo.getPrevalentClass(entityClass);
if (prevalentClass != null) {
Map entityMap = (Map) this.entitiesGlobalMap.get(prevalentClass);
if (entityMap == null) {
entityMap = new ConcurrentHashMap();
this.entitiesGlobalMap.put(prevalentClass, entityMap);
}
return entityMap;
} else {
throw new PrevaylerConfigurationException("Object with no configured prevalent class: " + entityClass);
}
}
}