DATADOC-48 moving over cross-store code from previous prototype

This commit is contained in:
Thomas Risberg
2011-03-02 08:57:20 -05:00
parent 3a6113c5e8
commit 841c78acb6
17 changed files with 723 additions and 7 deletions

View File

@@ -0,0 +1,32 @@
package org.springframework.persistence;
import java.lang.reflect.Field;
/**
* Listener interface for asynchronous storage operations.
* Can be annotated with OnlyOnFailure as an optimization
* if the listener is only interested in compensating transactions
* in the event of write failure.
*
* @author Rod Johnson
*
* @param <V> new value type
*/
public interface AsynchStoreCompletionListener<V> {
/**
* Constant indicating no store completion action
*/
class NONE implements AsynchStoreCompletionListener<Object> {
public void onCompletion(AsynchStoreCompletionListener.StoreResult result, Object newValue, Field foreignStore) {}
}
enum StoreResult {
SUCCESS,
FAILURE,
INDETERMINATE
};
void onCompletion(StoreResult result, V newValue, Field foreignStore);
}

View File

@@ -0,0 +1,78 @@
package org.springframework.persistence;
import java.lang.reflect.Field;
import org.springframework.dao.DataAccessException;
/**
* Interface to be implemented for each persistence technology,
* handling operations for the relevant entity type.
* Parameters: Key=K, Entity class=E
*
* @author Rod Johnson
*/
public interface EntityOperations<K,E> {
/**
* Is this clazz supported by the current EntityOperations?
* @param entityClass
* @param fs ForeignStore annotation, may be null
* @return
*/
boolean supports(Class<?> entityClass, RelatedEntity fs);
/**
* Return null if not found
* @param <T>
* @param entityClass
* @param pk
* @return
* @throws DataAccessException
*/
E findEntity(Class<E> entityClass, K pk) throws DataAccessException;
/**
* Find the unique key for the given entity whose class this EntityOperations
* understands. For example, it might be the id property value.
* @param entity
* @return
* @throws DataAccessException
*/
K findUniqueKey(E entity) throws DataAccessException;
/**
*
* @param entityClass
* @return the type of the unique key for this supported entity
* @throws DataAccessException
*/
Class<?> uniqueKeyType(Class<K> entityClass) throws DataAccessException;
boolean isTransient(E entity) throws DataAccessException;
/**
* Persist. Will cause key to be non-null.
* @param owner Persistent root entity, which has the RelatedEntity field
* @param entity
* @param f Foreign store field for entity being persisted
* @param fs ForeignStore annotation
* @throws DataAccessException
* @return the new unique key
*/
K makePersistent(Object owner, E entity, Field f, RelatedEntity fs) throws DataAccessException;
/**
* Is this type of entity transactional?
* @return
*/
boolean isTransactional();
/**
* Should the field be cached in the entity? For some entity types
* such as streams, there should be no caching, and the value
* should be retrieved from the persistent store every time.
* @return
*/
boolean cacheInEntity();
}

View File

@@ -0,0 +1,38 @@
package org.springframework.persistence;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
import org.springframework.dao.DataAccessException;
/**
* Convenient base class for EntityOperations implementations
* that adds ordering support.
* @author Rod Johnson
*
* @param <K>
* @param <E>
*/
public abstract class OrderedEntityOperations<K, E> implements EntityOperations<K, E>, Ordered {
protected final Log log = LogFactory.getLog(getClass());
private int order = Integer.MAX_VALUE;
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
/**
* Convenient default. Subclasses with non-Long key types can override this if they wish.
*/
@Override
public Class<?> uniqueKeyType(Class<K> entityClass) throws DataAccessException {
return Long.class;
}
}

View File

@@ -0,0 +1,65 @@
package org.springframework.persistence;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation indicating that a field may be stored in a foreign store
* and specifying the necessary guarantees. Conceptual rather than
* implementation-specific.
* @see ForeignStoreKeyManager
*
* @author Rod Johnson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface RelatedEntity {
/**
*
* Optional information as to how to compute or locate the key value.
* Some strategies may take this into account.
*/
String keyExpression() default "";
/**
* Should we use the key of the present entity
* @return
*/
boolean sameKey() default false;
/**
* Policies for persistence
* @return
*/
PersistencePolicy policy() default @PersistencePolicy();
/**
* Name for the preferred data store. Merely a hint. May not be followed.
* @return
*/
String preferredStore() default "";
/**
* Is asynchronous store OK?
* @return
*/
boolean asynchStore() default false;
// TODO - indicates if an asynchronous write should begin
// only after commit of a transaction
boolean afterCommit() default false;
/**
* Completion listener class. Only used if asynchStore is true.
* Must have a no-arg constructor.
* @return
*/
@SuppressWarnings("unchecked")
Class<? extends AsynchStoreCompletionListener> storeCompletionListenerClass() default AsynchStoreCompletionListener.NONE.class;
String storeCompletionListenerBeanName() default "";
}

View File

@@ -0,0 +1,129 @@
package org.springframework.persistence.support;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import org.aspectj.lang.reflect.FieldSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* Aspect that saves field access in a ChangeSet
*
* @author Rod Johnson
* @author Thomas Risberg
*/
public abstract aspect AbstractDeferredUpdateMixinFields<ET extends Annotation> extends AbstractTypeAnnotatingMixinFields<ET, ChangeSetBacked> {
//-------------------------------------------------------------------------
// Configure aspect for whole system.
// init() method can be invoked automatically if the aspect is a Spring
// bean, or called in user code.
//-------------------------------------------------------------------------
// Aspect shared config
private ChangeSetPersister<Object> changeSetPersister;
private ChangeSetSynchronizer<ChangeSetBacked> changeSetManager;
public void setChangeSetConfiguration(ChangeSetConfiguration<Object> changeSetConfiguration) {
this.changeSetPersister = changeSetConfiguration.getChangeSetPersister();
this.changeSetManager = changeSetConfiguration.getChangeSetManager();
}
//-------------------------------------------------------------------------
// Advise user-defined constructors of ChangeSetBacked objects to create a new
// backing ChangeSet
//-------------------------------------------------------------------------
pointcut arbitraryUserConstructorOfChangeSetBackedObject(ChangeSetBacked entity) :
execution((@ET ChangeSetBacked+).new(..)) &&
!execution((@ET ChangeSetBacked+).new(ChangeSet)) &&
this(entity);
// Or could use cflow
pointcut finderConstructorOfChangeSetBackedObject(ChangeSetBacked entity, ChangeSet cs) :
execution((@ET ChangeSetBacked+).new(ChangeSet)) &&
this(entity) &&
args(cs);
before(ChangeSetBacked entity) : arbitraryUserConstructorOfChangeSetBackedObject(entity) {
entity.itdChangeSetPersister = changeSetPersister;
log.info("User-defined constructor called on ChangeSetBacked object of class " + entity.getClass());
// Populate all properties
ChangeSet changeSet = new HashMapChangeSet();
changeSetManager.populateChangeSet(changeSet, entity);
entity.setChangeSet(changeSet);
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
throw new InvalidDataAccessResourceUsageException("No transaction synchronization is active");
}
TransactionSynchronizationManager.registerSynchronization(new ChangedSetBackedTransactionSynchronization(changeSetPersister, entity));
}
before(ChangeSetBacked entity, ChangeSet changeSet) : finderConstructorOfChangeSetBackedObject(entity, changeSet) {
entity.itdChangeSetPersister = changeSetPersister;
changeSetManager.populateEntity(changeSet, entity);
// Now leave an empty ChangeSet to listen only to future changes
entity.setChangeSet(new HashMapChangeSet());
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
throw new InvalidDataAccessResourceUsageException("No transaction synchronization is active");
}
TransactionSynchronizationManager.registerSynchronization(new ChangedSetBackedTransactionSynchronization(changeSetPersister, entity));
}
//-------------------------------------------------------------------------
// ChangeSet-related mixins
//-------------------------------------------------------------------------
// Introduced field
private ChangeSet ChangeSetBacked.changeSet;
private ChangeSetPersister<?> ChangeSetBacked.itdChangeSetPersister;
public void ChangeSetBacked.setChangeSet(ChangeSet cs) {
this.changeSet = cs;
}
public ChangeSet ChangeSetBacked.getChangeSet() {
return changeSet;
}
// Flush the entity state to the persistent store
public void ChangeSetBacked.flush() {
itdChangeSetPersister.persistState(this.getClass(), this.changeSet);
}
public Object ChangeSetBacked.getId() {
return itdChangeSetPersister.getPersistentId(this.getClass(), this.changeSet);
}
//-------------------------------------------------------------------------
// Around advice for field get/set
//-------------------------------------------------------------------------
// Nothing to do on field get unless laziness desired
Object around(ChangeSetBacked entity, Object newVal) : entityFieldSet(entity, newVal) {
Field f = ((FieldSignature) thisJoinPoint.getSignature()).getField();
String propName = f.getName();//getRedisPropertyName(thisJoinPoint.getSignature());
if (newVal instanceof Number) {
log.info("SET " + f + " -> ChangeSet number value property [" + propName + "] with value=[" + newVal + "]");
entity.getChangeSet().set(propName, (Number) newVal);
}
else if (newVal instanceof String) {
log.info("SET " + f + " -> ChangeSet string value property [" + propName + "] with value=[" + newVal + "]");
entity.getChangeSet().set(propName, (String) newVal);
}
else {
log.info("Don't know how to SET " + f + " with value=[" + newVal + "]");
}
return proceed(entity, newVal);
}
}

View File

@@ -0,0 +1,24 @@
package org.springframework.persistence.support;
import java.util.Map;
import org.springframework.core.convert.ConversionService;
/**
* Interface representing the set of changes in an entity.
*
* @author Rod Johnson
* @author Thomas Risberg
*
*/
public interface ChangeSet {
<T> T get(String key, Class<T> requiredClass, ConversionService cs);
void set(String key, Object o);
Map<String, Object> getValues();
Object removeProperty(String k);
}

View File

@@ -0,0 +1,13 @@
package org.springframework.persistence.support;
/**
* Interface introduced to objects exposing ChangeSet information
* @author Rod Johnson
* @author Thomas Risberg
*/
public interface ChangeSetBacked {
ChangeSet getChangeSet();
}

View File

@@ -0,0 +1,28 @@
package org.springframework.persistence.support;
public class ChangeSetConfiguration<T> {
private ChangeSetPersister<T> changeSetPersister;
private ChangeSetSynchronizer<ChangeSetBacked> changeSetManager;
public ChangeSetPersister<T> getChangeSetPersister() {
return changeSetPersister;
}
public void setChangeSetPersister(ChangeSetPersister<T> changeSetPersister) {
this.changeSetPersister = changeSetPersister;
}
public ChangeSetSynchronizer<ChangeSetBacked> getChangeSetManager() {
return changeSetManager;
}
public void setChangeSetManager(
ChangeSetSynchronizer<ChangeSetBacked> changeSetManager) {
this.changeSetManager = changeSetManager;
}
}

View File

@@ -0,0 +1,47 @@
package org.springframework.persistence.support;
import org.springframework.dao.DataAccessException;
/**
* Interface to be implemented by classes that can synchronize
* between data stores and ChangeSets.
* @author Rod Johnson
*
* @param <K> entity key
*/
public interface ChangeSetPersister<K> {
String ID_KEY = "_id";
String CLASS_KEY = "_class";
/**
* TODO how to tell when not found? throw exception?
*/
void getPersistentState(Class<? extends ChangeSetBacked> entityClass, K key, ChangeSet changeSet) throws DataAccessException, NotFoundException;
/**
* Return id
* @param cs
* @return
* @throws DataAccessException
*/
K getPersistentId(Class<? extends ChangeSetBacked> entityClass, ChangeSet cs) throws DataAccessException;
/**
* Return key
* @param cs Key may be null if not persistent
* @return
* @throws DataAccessException
*/
K persistState(Class<? extends ChangeSetBacked> entityClass, ChangeSet cs) throws DataAccessException;
/**
* Exception thrown in alternate control flow if getPersistentState
* finds no entity data.
*/
class NotFoundException extends Exception {
}
}

View File

@@ -0,0 +1,28 @@
package org.springframework.persistence.support;
import java.util.Map;
import org.springframework.dao.DataAccessException;
/**
* Interface to be implemented by classes that can synchronize
* between entities and ChangeSets.
* @author Rod Johnson
*
* @param <E>
*/
public interface ChangeSetSynchronizer<E extends ChangeSetBacked> {
Map<String, Class<?>> persistentFields(Class<? extends E> entityClassClass);
/**
* Take all entity fields into a changeSet.
* @param entity
* @return
* @throws DataAccessException
*/
void populateChangeSet(ChangeSet changeSet, E entity) throws DataAccessException;
void populateEntity(ChangeSet changeSet, E entity) throws DataAccessException;
}

View File

@@ -0,0 +1,66 @@
package org.springframework.persistence.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.support.TransactionSynchronization;
public class ChangedSetBackedTransactionSynchronization implements TransactionSynchronization {
protected final Log log = LogFactory.getLog(getClass());
private ChangeSetPersister<Object> changeSetPersister;
private ChangeSetBacked entity;
private int changeSetTxStatus = -1;
public ChangedSetBackedTransactionSynchronization(ChangeSetPersister<Object> changeSetPersister, ChangeSetBacked entity) {
this.changeSetPersister = changeSetPersister;
this.entity = entity;
}
@Override
public void afterCommit() {
log.debug("After Commit called for " + entity);
changeSetPersister.persistState(entity.getClass(), entity.getChangeSet());
changeSetTxStatus = 0;
}
@Override
public void afterCompletion(int status) {
log.debug("After Completion called with status = " + status);
if (changeSetTxStatus == 0) {
if (status == STATUS_COMMITTED) {
// this is good
log.debug("ChangedSetBackedTransactionSynchronization completed successfully for " + this.entity);
}
else {
// this could be bad - TODO: compensate
log.error("ChangedSetBackedTransactionSynchronization failed for " + this.entity);
}
}
}
@Override
public void beforeCommit(boolean readOnly) {
}
@Override
public void beforeCompletion() {
}
@Override
public void flush() {
}
@Override
public void resume() {
throw new IllegalStateException("ChangedSetBackedTransactionSynchronization does not support transaction suspension currently.");
}
@Override
public void suspend() {
throw new IllegalStateException("ChangedSetBackedTransactionSynchronization does not support transaction suspension currently.");
}
}

View File

@@ -0,0 +1,51 @@
package org.springframework.persistence.support;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.convert.ConversionService;
/**
* Simple ChangeSet implementation backed by a HashMap.
* @author Thomas Risberg
* @author Rod Johnson
*/
public class HashMapChangeSet implements ChangeSet {
private Map<String, Object> values;
public HashMapChangeSet(Map<String,Object> values) {
this.values = values;
}
public HashMapChangeSet() {
this(new HashMap<String, Object>());
}
@Override
public void set(String key, Object o) {
values.put(key, o);
}
@Override
public String toString() {
return "HashMapChangeSet: values=[" + values + "]";
}
@Override
public Map<String, Object> getValues() {
return Collections.unmodifiableMap(values);
}
@Override
public Object removeProperty(String k) {
return this.values.remove(k);
}
@Override
public <T> T get(String key, Class<T> requiredClass, ConversionService conversionService) {
return conversionService.convert(values.get(key), requiredClass);
}
}

View File

@@ -0,0 +1,96 @@
package org.springframework.persistence.support;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.DataAccessException;
import org.springframework.persistence.RelatedEntity;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.ReflectionUtils.FieldFilter;
/**
* Synchronizes fields to ChangeSets, regardless of visibility.
*
* @author Rod Johnson
*/
public class SimpleReflectiveChangeSetSynchronizer implements ChangeSetSynchronizer<ChangeSetBacked> {
/**
* Filter matching infrastructure fields, so they can be excluded
*/
private static FieldFilter PERSISTABLE_FIELDS = new FieldFilter() {
@Override
public boolean matches(Field f) {
return !(
f.isSynthetic() ||
Modifier.isStatic(f.getModifiers()) ||
Modifier.isTransient(f.getModifiers()) ||
f.getName().startsWith("ajc$") ||
f.isAnnotationPresent(RelatedEntity.class)
);
}
};
private final Log log = LogFactory.getLog(getClass());
private final ConversionService conversionService;
@Autowired
public SimpleReflectiveChangeSetSynchronizer(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public Map<String, Class<?>> persistentFields(Class<? extends ChangeSetBacked> entityClass) {
final Map<String, Class<?>> fields = new HashMap<String, Class<?>>();
ReflectionUtils.doWithFields(entityClass, new FieldCallback() {
@Override
public void doWith(Field f) throws IllegalArgumentException, IllegalAccessException {
fields.put(f.getName(), f.getType());
}
}, PERSISTABLE_FIELDS);
return fields;
}
@Override
public void populateChangeSet(final ChangeSet changeSet, final ChangeSetBacked entity) throws DataAccessException {
ReflectionUtils.doWithFields(entity.getClass(), new FieldCallback() {
@Override
public void doWith(Field f) throws IllegalArgumentException, IllegalAccessException {
f.setAccessible(true);
if (log.isDebugEnabled()) {
log.debug("POPULATE ChangeSet value from entity field: " + f);
}
changeSet.set(f.getName(), f.get(entity));
}
}, PERSISTABLE_FIELDS);
String classShortName = ClassUtils.getShortName(entity.getClass());
changeSet.set("_class", classShortName);
}
@Override
public void populateEntity(final ChangeSet changeSet, final ChangeSetBacked entity) throws DataAccessException {
ReflectionUtils.doWithFields(entity.getClass(), new FieldCallback() {
@Override
public void doWith(Field f) throws IllegalArgumentException, IllegalAccessException {
if (changeSet.getValues().containsKey(f.getName())) {
f.setAccessible(true);
if (log.isDebugEnabled()) {
log.debug("POPULATE entity from ChangeSet for field: " + f);
}
Object val = changeSet.get(f.getName(), f.getType(), conversionService);
f.set(entity, val);
}
}
}, PERSISTABLE_FIELDS);
}
}