diff --git a/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs b/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs index c4dacddd4..942f0c46e 100644 --- a/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs +++ b/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,4 @@ -#Tue Mar 01 12:59:14 EST 2011 +#Tue Mar 08 11:29:43 EST 2011 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 diff --git a/spring-data-commons-aspects/pom.xml b/spring-data-commons-aspects/pom.xml index 9bbc0d239..54152dc05 100644 --- a/spring-data-commons-aspects/pom.xml +++ b/spring-data-commons-aspects/pom.xml @@ -80,6 +80,17 @@ jsr250-api true + + javax.validation + validation-api + 1.0.0.GA + + + org.hibernate + hibernate-validator + 4.0.2.GA + test + diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingEntityOperationsLocator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingEntityOperationsLocator.java new file mode 100644 index 000000000..e28b401a4 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingEntityOperationsLocator.java @@ -0,0 +1,51 @@ +package org.springframework.persistence; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.OrderComparator; +import org.springframework.dao.DataAccessException; + +/** + * Chaining implementation of entity operations that automatically configures itself + * from a Spring context if available. + * + * @author Rod Johnson + */ +public class ChainingEntityOperationsLocator implements EntityOperationsLocator { + + private List entityOperationsList = new LinkedList(); + + @Autowired + public void init(ApplicationContext context) { + Map beansOfType = context.getBeansOfType(EntityOperations.class); + List l = new LinkedList(); + for (EntityOperations eo : beansOfType.values()) { + l.add(eo); + } + Collections.sort(l, new OrderComparator()); + for (EntityOperations eo : l) { + add(eo); + } + } + + public void add(EntityOperations ef) { + entityOperationsList.add(ef); + } + + @Override + public EntityOperations entityOperationsFor(Class entityClass, RelatedEntity fs) + throws DataAccessException { + for (EntityOperations eo : entityOperationsList) { + if (eo.supports(entityClass, fs)) { + return eo; + } + } + throw new UnknownEntityClassException(entityClass); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingForeignStoreKeyManagerLocator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingForeignStoreKeyManagerLocator.java new file mode 100644 index 000000000..3a5357cfa --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingForeignStoreKeyManagerLocator.java @@ -0,0 +1,52 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.OrderComparator; +import org.springframework.dao.DataAccessException; + +/** + * Chaining implementation of ForeignStoreKeyManagerLocator that can be parameterized + * from a Spring ApplicationContext. + * + * @author Rod Johnson + * + */ +public class ChainingForeignStoreKeyManagerLocator implements ForeignStoreKeyManagerLocator { + + private List delegates = new LinkedList(); + + public void add(ForeignStoreKeyManager fskm) { + delegates.add(fskm); + } + + @Autowired + public void init(ApplicationContext context) { + Map beansOfType = context.getBeansOfType(ForeignStoreKeyManager.class); + List l = new LinkedList(); + for (ForeignStoreKeyManager fskm : beansOfType.values()) { + l.add(fskm); + } + Collections.sort(l, new OrderComparator()); + for (ForeignStoreKeyManager fskm : l) { + add(fskm); + } + } + + @Override + public ForeignStoreKeyManager foreignStoreKeyManagerFor(Class entityClass, Field f) throws DataAccessException { + for (ForeignStoreKeyManager fskm : delegates) { + if (fskm.isSupportedField(entityClass, f)) { + return fskm; + } + } + throw new IllegalArgumentException("No ForeignStoreKeyManager for " + entityClass + " on " + f); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityManagerJpaEntityOperations.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityManagerJpaEntityOperations.java new file mode 100644 index 000000000..3c956ab57 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityManagerJpaEntityOperations.java @@ -0,0 +1,88 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.dao.DataAccessException; + +/** + * Implementation of entity operations that works on any entity + * that adheres to Roo persist() and static finder conventions. + * Does not depend on Roo, merely on Roo conventions, which can + * also be implemented by hand. + * + * @author Rod Johnson + */ +public class EntityManagerJpaEntityOperations extends OrderedEntityOperations { + + @PersistenceContext + private EntityManager entityManager; + + public static Object invoke(Class clazz, String methodName, + Object target, Class[] argTypes, Object... args) { + try { + Method m = clazz.getMethod(methodName, argTypes); + return m.invoke(target, (Object[]) args); + } catch (Exception ex) { + // TODO FIX ME + // System.out.println(ex + ": checked exceptions are stupid"); + throw new IllegalArgumentException(ex); + } + } + + public static Object invokeNoArgMethod(Class clazz, String methodName, Object target) { + return invoke(clazz, methodName, target, (Class[]) null, + (Object[]) null); + } + + @Override + public Object findEntity(Class entityClass, Object pk) + throws DataAccessException { + String findMethod = "find" + entityClass.getSimpleName(); + Object found = entityManager.find(entityClass, pk); + log.info("Lookup [" + entityClass.getName() + "] by pk=[" + pk + "] using EntityManager.find() - found [" + found + "]"); + return found; + } + + + @Override + public Object findUniqueKey(Object entity) throws DataAccessException { + String idMethodName = "getId"; + return invokeNoArgMethod(entity.getClass(), idMethodName, entity); + } + + @Override + public boolean isTransient(Object entity) throws DataAccessException { + return findUniqueKey(entity) == null; + } + + @Override + public Object makePersistent(Object owner, Object entity, Field f, RelatedEntity fs) throws DataAccessException { + if (log.isDebugEnabled()) { + log.debug("Making entity persistent: BEFORE [" + entity + "]"); + } + entityManager.persist(entity); + Object key = findUniqueKey(entity); + log.info("Making entity persistent: AFTER [" + entity + "]"); + return key; + } + + @Override + public boolean supports(Class entityClass, RelatedEntity fs) { + return entityClass.isAnnotationPresent(Entity.class); + } + + @Override + public boolean cacheInEntity() { + return false; + } + + @Override + public boolean isTransactional() { + // TODO Need to have a better test + return true; + }} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperationsLocator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperationsLocator.java new file mode 100644 index 000000000..64a70cd26 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperationsLocator.java @@ -0,0 +1,22 @@ +package org.springframework.persistence; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented by classes that can find EntityOperations + * implementations to handle particular classes. + * + * @author Rod Johnson + */ +public interface EntityOperationsLocator { + + /** + * Find the EntityOperations for this class. + * @param fs ForeignStore annotation (may be null) + * @param entityClass + * @return + * @throws DataAccessException if no EntityOperations can be found. + */ + EntityOperations entityOperationsFor(Class entityClass, RelatedEntity fs) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManager.java new file mode 100644 index 000000000..5efe6b21c --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManager.java @@ -0,0 +1,58 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.util.Set; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented to infer or compute foreign store + * key values and possibly store them. + * + * @author Rod Johnson + * + */ +public interface ForeignStoreKeyManager { + + /** + * Is this entity class one we can store additional + * state in related to fields annotated with ForeignStore. + * @param entityClass + * @param foreignStore + * @return + */ + boolean isSupportedField(Class entityClass, Field foreignStore); + + /** + * + * @param entity + * @param foreignStore + * @return null if not yet persistent + * @throws DataAccessException if the key cannot be computed or stored + */ + K findForeignStoreKey(T entity, Field foreignStore, Class keyClass) throws DataAccessException; + + /** + * Can be a NOP if the key is inferred + * @param entity + * @param foreignStore + * @param pk + * @throws DataAccessException + */ + void storeForeignStoreKey(T entity, Field foreignStore, Object pk) throws DataAccessException; + + /** + * Clear out the foreign key value + * Can be a NOP if the key is inferred + * @param entity + * @param foreignStore + * @param keyClass class of the key + * @throws DataAccessException + */ + void clearForeignStoreKey(T entity, Field foreignStore, Class keyClass) throws DataAccessException; + + Set findForeignStoreKeySet(T entity, Field foreignStore, Class keyClass) throws DataAccessException; + + void storeForeignStoreKeySet(T entity, Field foreignStore, Set keys) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManagerLocator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManagerLocator.java new file mode 100644 index 000000000..fbe475dc1 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManagerLocator.java @@ -0,0 +1,24 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented by classes that can find ForeignStoreKeyManager + * implementations to handle particular entities. + * + * @author Rod Johnson + */ +public interface ForeignStoreKeyManagerLocator { + + /** + * Find the ForeignStoreKeyManager for this class. + * @param f field the RelatedEntity annotation is on + * @param entityClass + * @return + * @throws DataAccessException if no ForeignStoreKeyManager can be found. + */ + ForeignStoreKeyManager foreignStoreKeyManagerFor(Class entityClass, Field f) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/GeneratedFieldForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/GeneratedFieldForeignStoreKeyManager.java new file mode 100644 index 000000000..8a8209bb2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/GeneratedFieldForeignStoreKeyManager.java @@ -0,0 +1,65 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataAccessException; + +/** + * Stores keys in generated additional persistent fields + * that Roo will add. e.g. + * + *
+ * atForeignStore
+ * Person Person;
+ * 
+ * long person_id;
+ * 
+ * 
+ * @author + * + */ +public class GeneratedFieldForeignStoreKeyManager extends + OrderedForeignStoreKeyManager { + + private final Log log = LogFactory.getLog(getClass()); + + + @Override + public Object findForeignStoreKey(Roo_GeneratedForeignStoreKeys entity, Field foreignStore, Class requiredClass) + throws DataAccessException { + String methodName = "get" + propertyName(foreignStore); + Object key = RooConventionEntityOperations.invokeNoArgMethod(entity.getClass(), methodName, entity); + log.info("FIND foreign store property " + foreignStore + " <- Entity generated String property [" + methodName + "] returned [" + key + "]"); + return key; + } + + @Override + public void storeForeignStoreKey(Roo_GeneratedForeignStoreKeys entity, Field foreignStore, + Object key) throws DataAccessException { + String methodName = "set" + propertyName(foreignStore); + RooConventionEntityOperations.invoke(entity.getClass(), methodName, entity, new Class[] { key.getClass()}, key); + log.info("STORE foreign store property " + foreignStore + " -> Entity generated String property [" + methodName + "] with key value [" + key + "]"); + } + + @Override + public void clearForeignStoreKey(Roo_GeneratedForeignStoreKeys entity, Field foreignStore, Class keyClass) throws DataAccessException { + String methodName = "set" + propertyName(foreignStore); + RooConventionEntityOperations.invoke(entity.getClass(), methodName, entity, new Class[] { keyClass }, null); + log.info("CKEAR foreign store property " + foreignStore + " -> Entity generated String property [" + methodName + "]"); + } + + + @Override + public boolean isSupportedField(Class clazz, Field f) { + // Check for marker interface + return Roo_GeneratedForeignStoreKeys.class.isAssignableFrom(clazz); + } + + + private String propertyName(Field f) { + return "_" + f.getName() + "_Id"; + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/InvalidFieldAnnotationException.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/InvalidFieldAnnotationException.java new file mode 100644 index 000000000..3741b96c2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/InvalidFieldAnnotationException.java @@ -0,0 +1,20 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Exception thrown on an attempt to use a field with an invalid + * RelatedEntity annotation. + * + * @author Rod Johnson + */ +public class InvalidFieldAnnotationException extends + InvalidDataAccessApiUsageException { + + public InvalidFieldAnnotationException(Class entityClass, Field f, String reason) { + super("Field [" + f.getName() + "] has invalid RelatedEntity annotation: reason='" + reason + "'", null); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/JpaEntityForeignStoreFieldTransience.aj b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/JpaEntityForeignStoreFieldTransience.aj new file mode 100644 index 000000000..05b6f47d2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/JpaEntityForeignStoreFieldTransience.aj @@ -0,0 +1,17 @@ +package org.springframework.persistence; + +import javax.persistence.Transient; +import javax.persistence.Entity; + +/** + * Aspect to annotate @ForeignStore fields as JPA @Transient to stop + * JPA trying to manage them itself + * @author Rod Johnson + * + */ +public privileged aspect JpaEntityForeignStoreFieldTransience { + + declare @field : @RelatedEntity * (@Entity *).* : @Transient; + + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/MappingValidator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/MappingValidator.java new file mode 100644 index 000000000..8143d0543 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/MappingValidator.java @@ -0,0 +1,20 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Interface to validate RelatedAnnotation annotation usage + * and other mapping constructs. + * + * @author Rod Johnson + * + */ +public interface MappingValidator { + + void validateGet(Class entityClass, Field f, RelatedEntity re) throws InvalidDataAccessApiUsageException; + + void validateSetTo(Class entityClass, Field f, RelatedEntity re, Object newVal) throws InvalidDataAccessApiUsageException, IllegalArgumentException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedForeignStoreKeyManager.java new file mode 100644 index 000000000..c4211a01a --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedForeignStoreKeyManager.java @@ -0,0 +1,45 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.util.Set; + +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 ForeignStoreKeyManager implementations that adds + * ordering support. + * + * @author Rod Johnson + */ +public abstract class OrderedForeignStoreKeyManager implements ForeignStoreKeyManager, 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; + } + + /** + * Subclasses can override if they support collection management. + */ + @Override + public Set findForeignStoreKeySet(T entity, Field foreignStore, Class keyClass) throws DataAccessException { + throw new UnsupportedOperationException(); + } + + @Override + public void storeForeignStoreKeySet(T entity, Field foreignStore, Set keys) throws DataAccessException { + throw new UnsupportedOperationException(); + } + +} \ No newline at end of file diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/PresentKeyForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/PresentKeyForeignStoreKeyManager.java new file mode 100644 index 000000000..c2308ee42 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/PresentKeyForeignStoreKeyManager.java @@ -0,0 +1,44 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.DataAccessException; + +/** + * ForeignStoreKeyManager implementation that uses the key of the present + * entity. + * + * @author Rod Johnson + * + */ +public class PresentKeyForeignStoreKeyManager extends OrderedForeignStoreKeyManager { + + private final EntityOperationsLocator eoLocator; + + public PresentKeyForeignStoreKeyManager(EntityOperationsLocator eoLocator) { + this.eoLocator = eoLocator; + } + + @Override + public Object findForeignStoreKey(Object entity, Field foreignStore, Class requiredClass) throws DataAccessException { + EntityOperations eo = eoLocator.entityOperationsFor(entity.getClass(), foreignStore.getAnnotation(RelatedEntity.class)); + return eo.findUniqueKey(entity); + } + + @Override + public boolean isSupportedField(Class clazz, Field foreignStore) { + RelatedEntity fs = foreignStore.getAnnotation(RelatedEntity.class); + return fs.sameKey(); + } + + @Override + public void storeForeignStoreKey(Object entity, Field foreignStore, Object pk) throws DataAccessException { + // Nothing to do + } + + @Override + public void clearForeignStoreKey(Object entity, Field foreignStore, Class keyClass) throws DataAccessException { + // Nothing to do + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RooConventionEntityOperations.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RooConventionEntityOperations.java new file mode 100644 index 000000000..e1d811bd7 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RooConventionEntityOperations.java @@ -0,0 +1,99 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.springframework.dao.DataAccessException; + +/** + * Implementation of entity operations that works on any entity + * that adheres to Roo persist() and static finder conventions. + * Does not depend on Roo, merely on Roo conventions, which can + * also be implemented by hand. + * + * @author Rod Johnson + */ +public class RooConventionEntityOperations extends OrderedEntityOperations { + + /** + * Utility method + * + * @param clazz + * @param methodName + * @param target + * @param args + * @return + */ + public static Object invoke(Class clazz, String methodName, + Object target, Class[] argTypes, Object... args) { + try { + Method m = clazz.getMethod(methodName, argTypes); + return m.invoke(target, (Object[]) args); + } catch (Exception ex) { + // TODO FIX ME + // System.out.println(ex + ": checked exceptions are stupid"); + throw new IllegalArgumentException(ex); + } + } + + public static Object invokeNoArgMethod(Class clazz, String methodName, Object target) { + return invoke(clazz, methodName, target, (Class[]) null, + (Object[]) null); + } + + @Override + public Object findEntity(Class entityClass, Object pk) + throws DataAccessException { + String findMethod = "find" + entityClass.getSimpleName(); + Object found = invoke(entityClass, findMethod, entityClass, + new Class[] { pk.getClass() }, pk); + log.info("Lookup [" + entityClass.getName() + "] by pk=[" + pk + "] using static finder method '" + + findMethod + "' found [" + found + "]"); + return found; + } + + + @Override + public Object findUniqueKey(Object entity) throws DataAccessException { + String idMethodName = "getId"; + return invokeNoArgMethod(entity.getClass(), idMethodName, entity); + } + + @Override + public boolean isTransient(Object entity) throws DataAccessException { + return findUniqueKey(entity) == null; + } + + @Override + public Object makePersistent(Object owner, Object entity, Field f, RelatedEntity fs) throws DataAccessException { + if (log.isDebugEnabled()) { + log.debug("Making entity persistent: BEFORE [" + entity + "]"); + } + String persistMethodName = "persist"; + invokeNoArgMethod(entity.getClass(), persistMethodName, entity); + Object key = findUniqueKey(entity); + log.info("Making entity persistent: AFTER [" + entity + "]"); + return key; + } + + @Override + public boolean supports(Class clazz, RelatedEntity fs) { + try { + // TODO fix this + clazz.getMethod("getId"); + return true; + } catch (Exception ex) { + return false; + } + } + + @Override + public boolean cacheInEntity() { + return false; + } + + @Override + public boolean isTransactional() { + // TODO Need to have a better test + return true; + }} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/Roo_GeneratedForeignStoreKeys.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/Roo_GeneratedForeignStoreKeys.java new file mode 100644 index 000000000..ab9706613 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/Roo_GeneratedForeignStoreKeys.java @@ -0,0 +1,10 @@ +package org.springframework.persistence; + +/** + * Tag interface introduced to objects that have introduced foreign store keys + * @author Rod Johnson + * + */ +public interface Roo_GeneratedForeignStoreKeys { + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/StoreSpanning.aj b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/StoreSpanning.aj new file mode 100644 index 000000000..2152def30 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/StoreSpanning.aj @@ -0,0 +1,172 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.reflect.FieldSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.persistence.support.DefaultManagedSet; +import org.springframework.persistence.support.ManagedSet; +import org.springframework.persistence.support.ManagedSet.ChangeListener; + +/** + * Aspect to handle ForeignStore annotation indicating navigation to a + * potentially different persistence store. + * + * Can be configured via invoking init() method or through Spring + * autowiring if beans named "entityOperationsLocator" and + * "foreignStoreKeyManager" are provided. + * + * @author Rod Johnson + */ +public privileged aspect StoreSpanning { + + private final Log log = LogFactory.getLog(getClass()); + + private EntityOperationsLocator entityOperationsLocator; + + private ForeignStoreKeyManagerLocator foreignStoreKeyManagerLocator; + + private MappingValidator mappingValidator; + + @Autowired + public void init(EntityOperationsLocator eol, ForeignStoreKeyManagerLocator fskml) { + this.entityOperationsLocator = eol; + this.foreignStoreKeyManagerLocator = fskml; + } + + @Autowired(required=false) + public void setMappingValidator(MappingValidator mv) { + this.mappingValidator = mv; + } + + + public pointcut foreignEntityFieldGet(Object entity, RelatedEntity fs) : + get(@RelatedEntity * *) && + this(entity) && + @annotation(fs); + + public pointcut foreignEntityFieldSet(Object entity, RelatedEntity fs, Object newVal) : + set(@RelatedEntity * *) && + this(entity) && + @annotation(fs) && + args(newVal); + + @SuppressWarnings("unchecked") + Object around(Object entity, RelatedEntity fs) : foreignEntityFieldGet(entity, fs) { + Field f = ((FieldSignature) thisJoinPoint.getSignature()).getField(); + log.info("GET: Handling foreign store " + f); + if (this.mappingValidator != null) { + this.mappingValidator.validateGet(entity.getClass(), f, fs); + } + + Object fieldValue = proceed(entity, fs); + // What if it was set to null? + if (fieldValue != null) { + log.info("GET " + f + ": returning actual field value"); + return fieldValue; + } + + // Must retrieve + if (Set.class.isAssignableFrom(f.getType())) { + // TODO empty set, store class + log.info("GET " + f + ": Retrieving ManagedSet"); + ForeignStoreKeyManager foreignStoreKeyManager = foreignStoreKeyManagerLocator.foreignStoreKeyManagerFor(entity.getClass(), f); + + // TODO fix me, this is fragile + ParameterizedType genericType = (ParameterizedType) f.getGenericType(); + Class entityClass = (Class) genericType.getActualTypeArguments()[0]; + Class keyClass = entityOperationsLocator.entityOperationsFor(entityClass, fs).uniqueKeyType(entityClass); + Set keySet = foreignStoreKeyManager.findForeignStoreKeySet(entity, f, keyClass); + ManagedSet managedSet = DefaultManagedSet.fromKeySet(keySet, entityClass, entityOperationsLocator); + return managedSet; + } + else if (Collection.class.isAssignableFrom(f.getType())) { + throw new UnsupportedOperationException("Unsupported collection type " + f.getType() + " in entity class " + entity.getClass()); + } + else { + return findScalarEntity(entity, f, fs); + } + } + + private Object findScalarEntity(Object entity, Field f, RelatedEntity fs) { + EntityOperations eo = entityOperationsLocator.entityOperationsFor(f.getType(), fs); + Class keyType = eo.uniqueKeyType(f.getType()); + ForeignStoreKeyManager foreignStoreKeyManager = foreignStoreKeyManagerLocator.foreignStoreKeyManagerFor(entity.getClass(), f); + Object pk = foreignStoreKeyManager.findForeignStoreKey(entity, f, keyType); + if (pk != null) { + log.debug("GET " + f + ": entity find for key=[" + pk + "] of class [" + pk.getClass() + "]"); + Object found = eo.findEntity(f.getType(), pk); + log.info("GET " + f + ": entity find for key=[" + pk + "] found [" + found + "]"); + return found; + } + else { + log.info("GET " + f + ": no key found, returning null"); + return null; + } + } + + // TODO handle explicit set to null + @SuppressWarnings("unchecked") + Object around(final Object entity, RelatedEntity fs, Object newVal) : foreignEntityFieldSet(entity, fs, newVal) { + final Field f = ((FieldSignature) thisJoinPoint.getSignature()).getField(); + if (this.mappingValidator != null) { + this.mappingValidator.validateSetTo(entity.getClass(), f, fs, newVal); + } + log.info("SET: Handling foreign store " + f); + + if (newVal != null) { + if (Set.class.isAssignableFrom(f.getType())) { + log.info("Setting set: Creating ManagedSet"); + final ManagedSet managedSet = DefaultManagedSet.fromEntitySet((Set) newVal, entityOperationsLocator); + final ForeignStoreKeyManager foreignStoreKeyManager = foreignStoreKeyManagerLocator.foreignStoreKeyManagerFor(entity.getClass(), f); + foreignStoreKeyManager.storeForeignStoreKeySet(entity, f, managedSet.getKeySet()); + managedSet.addListener(new ChangeListener() { + @Override + public void onDirty() { + foreignStoreKeyManager.storeForeignStoreKeySet(entity, f, managedSet.getKeySet()); + } + }); + return proceed(entity, fs, managedSet); + } + else if (Collection.class.isAssignableFrom(f.getType())) { + throw new UnsupportedOperationException("Unsupported collection type " + f.getType() + " in entity class " + entity.getClass()); + } + else { + EntityOperations eo = handleScalarFieldSet(entity, f, fs, newVal); + + // Don't store it in the entity if the entity type doesn't support + // it, for example + // because it shouldn't be read repeatedly (as with a stream) + if (!eo.cacheInEntity()) { + return null; + } + } + } + return proceed(entity, fs, newVal); + } + + private EntityOperations handleScalarFieldSet(Object entity, Field f, RelatedEntity fs, Object newVal) { + EntityOperations eo = entityOperationsLocator.entityOperationsFor(f.getType(), fs); + Object pk = eo.findUniqueKey(newVal); + + System.err.println("TODO: test whether current entity is persistent"); + if (pk == null) { + // Entity is transient for now + log.info("SET " + f + ": no foreign store key to store; entity has no persistent identity, MAKING PERSISTENT"); + pk = eo.makePersistent(entity,newVal, f, fs); + } + + if (pk != null) { + ForeignStoreKeyManager foreignStoreKeyManager = foreignStoreKeyManagerLocator.foreignStoreKeyManagerFor(entity.getClass(), f); + foreignStoreKeyManager.storeForeignStoreKey(entity, f, pk); + log.info("SET " + f + ": stored foreign store key=[" + pk + "]"); + } + return eo; + } +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/UnknownEntityClassException.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/UnknownEntityClassException.java new file mode 100644 index 000000000..f17c13d9f --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/UnknownEntityClassException.java @@ -0,0 +1,12 @@ +package org.springframework.persistence; + +import org.springframework.dao.UncategorizedDataAccessException; + +public class UnknownEntityClassException extends + UncategorizedDataAccessException { + + public UnknownEntityClassException(Class entityClass) { + super("Unknown entity class [" + entityClass.getName() + "]", null); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConstructorEntityInstantiator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConstructorEntityInstantiator.java new file mode 100644 index 000000000..47fb10322 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConstructorEntityInstantiator.java @@ -0,0 +1,17 @@ +package org.springframework.persistence.support; + + +/** + * Try for a constructor taking a ChangeSet: failing that, try a no-arg + * constructor and then setChangeSet(). + * + * @author Rod Johnson + */ +public class ChangeSetConstructorEntityInstantiator extends AbstractConstructorEntityInstantiator{ + + @Override + protected void setState(ChangeSetBacked entity, ChangeSet cs) { + entity.setChangeSet(cs); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetForeignStoreKeyManager.java new file mode 100644 index 000000000..695331726 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetForeignStoreKeyManager.java @@ -0,0 +1,94 @@ +package org.springframework.persistence.support; + +import java.lang.reflect.Field; +import java.util.Set; + +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.OrderedForeignStoreKeyManager; + +/** + * ForeignStoreKeyManager implementation that backs the foreign key to a + * ChangeSet. + * + * @author Thomas Risberg + * @author Rod Johnson + */ +public class ChangeSetForeignStoreKeyManager extends OrderedForeignStoreKeyManager { + + public static final String FOREIGN_STORE_SET_PREFIX = "S"; + + protected final Log log = LogFactory.getLog(getClass()); + + private String fieldDelimiter = "."; + + private final ConversionService conversionService; + + public String getFieldDelimiter() { + return fieldDelimiter; + } + + public void setFieldDelimiter(String fieldDelimiter) { + this.fieldDelimiter = fieldDelimiter; + } + + @Autowired + public ChangeSetForeignStoreKeyManager(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public void clearForeignStoreKey(ChangeSetBacked entity, Field foreignStore, Class keyClass) throws DataAccessException { + String propName = propertyName(entity, foreignStore); + entity.getChangeSet().removeProperty(propName); + log.info("CLEAR foreign store property " + foreignStore + " <- ChangeSetBacked foreign key property [" + propName + "]"); + } + + @Override + public K findForeignStoreKey(ChangeSetBacked entity, Field foreignStore, Class keyClass) throws DataAccessException { + String propName = propertyName(entity, foreignStore); + System.err.println("+++ " + entity.getChangeSet().getValues()); + K key = entity.getChangeSet().get(propName, keyClass, this.conversionService); + log.info("FIND foreign store property " + foreignStore + " <- ChangeSetBacked foreign key property [" + propName + "] returned [" + + key + "]"); + return key; + } + + @Override + public boolean isSupportedField(Class entityClass, Field foreignStore) { + return ChangeSetBacked.class.isAssignableFrom(entityClass); + } + + @Override + public void storeForeignStoreKey(ChangeSetBacked entity, Field foreignStore, Object pk) throws DataAccessException { + String propName = propertyName(entity, foreignStore); + entity.getChangeSet().set(propName, pk); + log.info("STORE foreign store property " + foreignStore + " -> ChangeSetBacked foreign key property [" + propName + + "] with key value [" + pk + "]"); + } + + @Override + public Set findForeignStoreKeySet(ChangeSetBacked entity, Field foreignStore, Class keyClass) throws DataAccessException { + Set keySet = entity.getChangeSet().get(foreignStoreKeyName(foreignStore), Set.class, this.conversionService); +// if (keySet != null && !keySet.isEmpty()) +// System.out.println("KeySET=**************" + keySet + ", 0th type=" + keySet.iterator().next().getClass()); + return keySet; + } + + private String foreignStoreKeyName(Field foreignStore) { + return FOREIGN_STORE_SET_PREFIX + getFieldDelimiter() + foreignStore.getName(); + } + + @Override + public void storeForeignStoreKeySet(ChangeSetBacked entity, Field foreignStore, Set keys) throws DataAccessException { + entity.getChangeSet().set(foreignStoreKeyName(foreignStore), keys); + } + + private String propertyName(ChangeSetBacked rb, Field f) { + return rb.getClass().getSimpleName() + getFieldDelimiter() + f.getName(); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/DefaultManagedSet.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/DefaultManagedSet.java new file mode 100644 index 000000000..072a36143 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/DefaultManagedSet.java @@ -0,0 +1,162 @@ +package org.springframework.persistence.support; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.springframework.dao.DataAccessException; +import org.springframework.persistence.EntityOperations; +import org.springframework.persistence.EntityOperationsLocator; + +public class DefaultManagedSet implements ManagedSet { + + public static DefaultManagedSet fromEntitySet(Set entitySet, EntityOperationsLocator eol) { + DefaultManagedSet dms = new DefaultManagedSet(eol); + if (entitySet != null) { + for (Object entity : entitySet) { + dms.add(entity); + } + } + return dms; + } + + public static DefaultManagedSet fromKeySet(Set keySet, Class entityClass, EntityOperationsLocator eol) throws DataAccessException { + DefaultManagedSet dms = new DefaultManagedSet(eol); + if (keySet != null) { + for (Object key : keySet) { + dms.keySet.add(key); + EntityOperations eo = eol.entityOperationsFor(entityClass, null); + dms.entitySet.add(eo.findEntity(entityClass, key)); + } + } + return dms; + } + + private final Set keySet = new HashSet(); + + private final Set entitySet = new HashSet(); + + private boolean dirty; + + private List listeners = new LinkedList(); + + private final EntityOperationsLocator entityOperationsLocator; + + private DefaultManagedSet(EntityOperationsLocator eol) { + this.entityOperationsLocator = eol; + } + + @Override + public void addListener(ChangeListener l) { + this.listeners.add(l); + } + + + protected void publishEvent() { + this.dirty = true; + for (ChangeListener l : listeners) { + l.onDirty(); + } + } + + @Override + public Set getKeySet() { + return this.keySet; + } + + @Override + public boolean isDirty() { + return this.dirty; + } + + + @Override + public boolean add(Object e) { + if (entitySet.contains(e)) { + return false; + } + EntityOperations eo = entityOperationsLocator.entityOperationsFor(e.getClass(), null); + Object key = eo.findUniqueKey(e); + if (key == null) { + eo.makePersistent(null, e, null, null); + } + key = eo.findUniqueKey(e); + keySet.add(key); + entitySet.add(e); + publishEvent(); + return true; + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + this.entitySet.clear(); + this.keySet.clear(); + publishEvent(); + } + + @Override + public boolean contains(Object o) { + return entitySet.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return entitySet.containsAll(c); + } + + @Override + public boolean isEmpty() { + return keySet.isEmpty(); + } + + @Override + public Iterator iterator() { + return entitySet.iterator(); + } + + @Override + public boolean remove(Object e) { + if (!entitySet.contains(e)) { + return false; + } + EntityOperations eo = entityOperationsLocator.entityOperationsFor(e.getClass(), null); + keySet.remove(eo.findUniqueKey(e)); + entitySet.remove(e); + publishEvent(); + return true; + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return keySet.size(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public Object[] toArray(Object[] a) { + throw new UnsupportedOperationException(); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ManagedSet.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ManagedSet.java new file mode 100644 index 000000000..430865fa2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ManagedSet.java @@ -0,0 +1,27 @@ +package org.springframework.persistence.support; + +import java.util.Set; + +public interface ManagedSet extends Set { + + Set getKeySet(); + + void addListener(ChangeListener l); + + boolean isDirty(); + + interface ChangeListener { + void onDirty(); + } + + // TODO move into managed collection + + // TODO insertions, deletions + // after markSynchronized() + + // This may be wrong, shouldn't it give something back for a ChangeSet? +// void retrieve(EntityOperationsLocator eol) throws DataAccessException; +// +// void persist(EntityOperationsLocator eol) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleMappingValidator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleMappingValidator.java new file mode 100644 index 000000000..e06212ca1 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleMappingValidator.java @@ -0,0 +1,34 @@ +package org.springframework.persistence.support; + +import java.lang.reflect.Field; + +import javax.validation.constraints.NotNull; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.persistence.AsynchStoreCompletionListener; +import org.springframework.persistence.InvalidFieldAnnotationException; +import org.springframework.persistence.MappingValidator; +import org.springframework.persistence.RelatedEntity; + +// TODO fancier version could discover many rules, with annotations etc. +// Also invoke the relevant EntityOperations +public class SimpleMappingValidator implements MappingValidator { + + @Override + public void validateGet(Class entityClass, Field f, RelatedEntity re) throws InvalidDataAccessApiUsageException { + // Validate the annotation + if (!AsynchStoreCompletionListener.NONE.class.equals(re.storeCompletionListenerClass()) + && !"".equals(re.storeCompletionListenerBeanName())) { + throw new InvalidFieldAnnotationException(entityClass, f, + "Can't have storeCompletionListener class and bean name on same annotation"); + } + } + + public void validateSetTo(Class entityClass, Field f, RelatedEntity re, Object newVal) throws InvalidDataAccessApiUsageException, + IllegalArgumentException { + if (newVal == null && f.isAnnotationPresent(NotNull.class)) { + throw new IllegalArgumentException("Can't set non-null field [" + f.getName() + " to null"); + } + } + +} diff --git a/spring-data-commons-aspects/template.mf b/spring-data-commons-aspects/template.mf index 84c7abd31..97bcff638 100644 --- a/spring-data-commons-aspects/template.mf +++ b/spring-data-commons-aspects/template.mf @@ -5,14 +5,19 @@ Bundle-ManifestVersion: 2 Import-Package: sun.reflect;version="0";resolution:=optional Excluded-Imports: - org.springframework.persistence + org.springframework.persistence, + org.springframework.persistence.support Import-Template: org.springframework.beans.*;version="[3.0.0, 4.0.0)", + org.springframework.context.*;version="[3.0.0, 4.0.0)", org.springframework.core.*;version="[3.0.0, 4.0.0)", org.springframework.dao.*;version="[3.0.0, 4.0.0)", + org.springframework.orm.*;version="[3.0.0, 4.0.0)", org.springframework.transaction..*;version="[3.0.0, 4.0.0)", org.springframework.util.*;version="[3.0.0, 4.0.0)", org.springframework.data.core.*;version="[1.0.0, 2.0.0)", + javax.validation.*;version="[1.0.0, 2.0.0)";resolution:=optional, + javax.persistence.*;version="[1.0.0, 3.0.0)";resolution:=optional, org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", org.aspectj.*;version="[1.6.5, 2.0.0)",