diff --git a/pom.xml b/pom.xml index 770c31066..d83db4c97 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ org.springframework.build.aws org.springframework.build.aws.maven - 2.0.0.RELEASE + 3.1.0.RELEASE @@ -133,7 +133,7 @@ generate-html generate-pdf - package + pre-site diff --git a/spring-data-commons-aspects/.classpath b/spring-data-commons-aspects/.classpath index 16f01e2ee..19ed14911 100644 --- a/spring-data-commons-aspects/.classpath +++ b/spring-data-commons-aspects/.classpath @@ -1,7 +1,7 @@ - - + + 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 e78b07017..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,6 +1,13 @@ -#Wed Nov 17 12:20:43 EST 2010 +#Tue Mar 08 11:29:43 EST 2011 eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 -org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.source=1.5 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/spring-data-commons-aspects/pom.xml b/spring-data-commons-aspects/pom.xml index 7d0c99b3a..54152dc05 100644 --- a/spring-data-commons-aspects/pom.xml +++ b/spring-data-commons-aspects/pom.xml @@ -80,6 +80,24 @@ jsr250-api true + + javax.validation + validation-api + 1.0.0.GA + + + org.hibernate + hibernate-validator + 4.0.2.GA + test + + + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + 1.0.0.Final + org.mockito @@ -108,6 +126,11 @@ Springframework Maven SNAPSHOT Repository http://maven.springframework.org/snapshot + + jboss-repository + JBoss Public Repository + http://repository.jboss.org/nexus/content/groups/public-jboss + diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/AsynchStoreCompletionListener.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/AsynchStoreCompletionListener.java new file mode 100644 index 000000000..4f417bb57 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/AsynchStoreCompletionListener.java @@ -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 new value type + */ +public interface AsynchStoreCompletionListener { + + /** + * Constant indicating no store completion action + */ + class NONE implements AsynchStoreCompletionListener { + public void onCompletion(AsynchStoreCompletionListener.StoreResult result, Object newValue, Field foreignStore) {} + } + + enum StoreResult { + SUCCESS, + FAILURE, + INDETERMINATE + }; + + void onCompletion(StoreResult result, V newValue, Field foreignStore); + +} 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/EntityOperations.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperations.java new file mode 100644 index 000000000..df6aa2f49 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperations.java @@ -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 { + + /** + * 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 + * @param entityClass + * @param pk + * @return + * @throws DataAccessException + */ + E findEntity(Class 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 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(); + +} 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/OrderedEntityOperations.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedEntityOperations.java new file mode 100644 index 000000000..e1cf11a83 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedEntityOperations.java @@ -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 + * @param + */ +public abstract class OrderedEntityOperations implements EntityOperations, 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 entityClass) throws DataAccessException { + return Long.class; + } +} \ No newline at end of file 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/RelatedEntity.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RelatedEntity.java new file mode 100644 index 000000000..f9e538c82 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RelatedEntity.java @@ -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 storeCompletionListenerClass() default AsynchStoreCompletionListener.NONE.class; + + String storeCompletionListenerBeanName() default ""; + +} 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/AbstractDeferredUpdateMixinFields.aj b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/AbstractDeferredUpdateMixinFields.aj new file mode 100644 index 000000000..bdfe6ce5b --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/AbstractDeferredUpdateMixinFields.aj @@ -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 extends AbstractTypeAnnotatingMixinFields { + + //------------------------------------------------------------------------- + // 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 changeSetPersister; + + private ChangeSetSynchronizer changeSetManager; + + public void setChangeSetConfiguration(ChangeSetConfiguration 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); + } + +} \ No newline at end of file diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSet.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSet.java new file mode 100644 index 000000000..2fa0ff0d6 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSet.java @@ -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 get(String key, Class requiredClass, ConversionService cs); + + void set(String key, Object o); + + Map getValues(); + + Object removeProperty(String k); + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetBacked.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetBacked.java new file mode 100644 index 000000000..cf1994e74 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetBacked.java @@ -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(); + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConfiguration.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConfiguration.java new file mode 100644 index 000000000..3bdd97f1b --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConfiguration.java @@ -0,0 +1,28 @@ +package org.springframework.persistence.support; + +public class ChangeSetConfiguration { + + private ChangeSetPersister changeSetPersister; + + private ChangeSetSynchronizer changeSetManager; + + public ChangeSetPersister getChangeSetPersister() { + return changeSetPersister; + } + + public void setChangeSetPersister(ChangeSetPersister changeSetPersister) { + this.changeSetPersister = changeSetPersister; + } + + public ChangeSetSynchronizer getChangeSetManager() { + return changeSetManager; + } + + public void setChangeSetManager( + ChangeSetSynchronizer changeSetManager) { + this.changeSetManager = changeSetManager; + } + + + +} 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/ChangeSetPersister.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetPersister.java new file mode 100644 index 000000000..2f63414b2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetPersister.java @@ -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 entity key + */ +public interface ChangeSetPersister { + + String ID_KEY = "_id"; + + String CLASS_KEY = "_class"; + + /** + * TODO how to tell when not found? throw exception? + */ + void getPersistentState(Class entityClass, K key, ChangeSet changeSet) throws DataAccessException, NotFoundException; + + /** + * Return id + * @param cs + * @return + * @throws DataAccessException + */ + K getPersistentId(Class entityClass, ChangeSet cs) throws DataAccessException; + + /** + * Return key + * @param cs Key may be null if not persistent + * @return + * @throws DataAccessException + */ + K persistState(Class entityClass, ChangeSet cs) throws DataAccessException; + + /** + * Exception thrown in alternate control flow if getPersistentState + * finds no entity data. + */ + class NotFoundException extends Exception { + + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetSynchronizer.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetSynchronizer.java new file mode 100644 index 000000000..098365e1a --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetSynchronizer.java @@ -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 + */ +public interface ChangeSetSynchronizer { + + Map> persistentFields(Class 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; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangedSetBackedTransactionSynchronization.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangedSetBackedTransactionSynchronization.java new file mode 100644 index 000000000..82cf1c3a6 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangedSetBackedTransactionSynchronization.java @@ -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 changeSetPersister; + + private ChangeSetBacked entity; + + private int changeSetTxStatus = -1; + + public ChangedSetBackedTransactionSynchronization(ChangeSetPersister 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."); + } + +} 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/HashMapChangeSet.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/HashMapChangeSet.java new file mode 100644 index 000000000..e9fe86104 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/HashMapChangeSet.java @@ -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 values; + + public HashMapChangeSet(Map values) { + this.values = values; + } + + public HashMapChangeSet() { + this(new HashMap()); + } + + @Override + public void set(String key, Object o) { + values.put(key, o); + } + + @Override + public String toString() { + return "HashMapChangeSet: values=[" + values + "]"; + } + + @Override + public Map getValues() { + return Collections.unmodifiableMap(values); + } + + @Override + public Object removeProperty(String k) { + return this.values.remove(k); + } + + @Override + public T get(String key, Class requiredClass, ConversionService conversionService) { + return conversionService.convert(values.get(key), requiredClass); + } + +} 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/src/main/java/org/springframework/persistence/support/SimpleReflectiveChangeSetSynchronizer.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleReflectiveChangeSetSynchronizer.java new file mode 100644 index 000000000..43b6f6b51 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleReflectiveChangeSetSynchronizer.java @@ -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 { + + /** + * 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> persistentFields(Class entityClass) { + final Map> fields = new HashMap>(); + 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); + } + +} diff --git a/spring-data-commons-aspects/template.mf b/spring-data-commons-aspects/template.mf index 1acd51461..97bcff638 100644 --- a/spring-data-commons-aspects/template.mf +++ b/spring-data-commons-aspects/template.mf @@ -4,13 +4,20 @@ Bundle-Vendor: SpringSource Bundle-ManifestVersion: 2 Import-Package: sun.reflect;version="0";resolution:=optional +Excluded-Imports: + 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)", - org.springframework.data.persistence.*;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)", diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java index 797598368..4f8c56e82 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2008-2011 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,16 +22,16 @@ import java.io.Serializable; * Simple interface for entities. * * @author Oliver Gierke - * @param the type of the identifier + * @param the type of the identifier */ -public interface Persistable extends Serializable { +public interface Persistable extends Serializable { /** * Returns the id of the entity. * * @return the id */ - PK getId(); + ID getId(); /** diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java index 8d733568f..d300a618a 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java @@ -111,6 +111,24 @@ public class Sort implements } + /** + * Returns the order registered for the given property. + * + * @param property + * @return + */ + public Order getOrderFor(String property) { + + for (Order order : this) { + if (order.getProperty().equals(property)) { + return order; + } + } + + return null; + } + + /* * (non-Javadoc) * diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java new file mode 100644 index 000000000..a15c175b6 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query; + +import java.util.Iterator; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + + +/** + * Interface to access method parameters. Allows dedicated access to parameters + * of special types + * + * @author Oliver Gierke + */ +public interface ParameterAccessor extends Iterable { + + /** + * Returns the {@link Pageable} of the parameters, if available. Returns + * {@code null} otherwise. + * + * @return + */ + Pageable getPageable(); + + + /** + * Returns the sort instance to be used for query creation. Will use a + * {@link Sort} parameter if available or the {@link Sort} contained in a + * {@link Pageable} if available. Returns {@code null} if no {@link Sort} + * can be found. + * + * @return + */ + Sort getSort(); + + + /** + * Returns the bindable value with the given index. Bindable means, that + * {@link Pageable} and {@link Sort} values are skipped without noticed in + * the index. For a method signature taking {@link String}, {@link Pageable} + * , {@link String}, {@code #getBindableParameter(1)} would return the + * second {@link String} value. + * + * @param index + * @return + */ + Object getBindableValue(int index); + + + /** + * Returns an iterator over all bindable parameters. This means + * parameters implementing {@link Pageable} or {@link Sort} will not be + * included in this {@link Iterator}. + * + * @return + */ + Iterator iterator(); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java similarity index 63% rename from spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java rename to spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java index 9f8e8032e..47c38be4f 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java @@ -15,29 +15,32 @@ */ package org.springframework.data.repository.query; +import java.util.Iterator; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.util.Assert; /** - * {@link SimpleParameterAccessor} is used to bind method parameters. + * {@link ParameterAccessor} implementation using a {@link Parameters} instance + * to find special parameters. * * @author Oliver Gierke */ -public class SimpleParameterAccessor { +public class ParametersParameterAccessor implements ParameterAccessor { private final Parameters parameters; private final Object[] values; /** - * Creates a new {@link SimpleParameterAccessor}. + * Creates a new {@link ParametersParameterAccessor}. * * @param parameters * @param values */ - public SimpleParameterAccessor(Parameters parameters, Object[] values) { + public ParametersParameterAccessor(Parameters parameters, Object[] values) { Assert.notNull(parameters); Assert.notNull(values); @@ -50,11 +53,11 @@ public class SimpleParameterAccessor { } - /** - * Returns the {@link Pageable} of the parameters, if available. Returns - * {@code null} otherwise. + /* + * (non-Javadoc) * - * @return + * @see + * org.springframework.data.repository.query.ParameterAccessor#getPageable() */ public Pageable getPageable() { @@ -66,13 +69,11 @@ public class SimpleParameterAccessor { } - /** - * Returns the sort instance to be used for query creation. Will use a - * {@link Sort} parameter if available or the {@link Sort} contained in a - * {@link Pageable} if available. Returns {@code null} if no {@link Sort} - * can be found. + /* + * (non-Javadoc) * - * @return + * @see + * org.springframework.data.repository.query.ParameterAccessor#getSort() */ public Sort getSort() { @@ -88,17 +89,24 @@ public class SimpleParameterAccessor { } - private Object getBindableValue(int index) { + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.query.ParameterAccessor#getBindableValue + * (int) + */ + public Object getBindableValue(int index) { return values[parameters.getBindableParameter(index).getIndex()]; } - /** - * Returns a {@link BindableParameterIterator} to traverse all bindable - * parameters. + /* + * (non-Javadoc) * - * @return + * @see + * org.springframework.data.repository.query.ParameterAccessor#iterator() */ public BindableParameterIterator iterator() { @@ -111,7 +119,7 @@ public class SimpleParameterAccessor { * * @author Oliver Gierke */ - public class BindableParameterIterator { + private class BindableParameterIterator implements Iterator { private int currentIndex = 0; @@ -125,5 +133,27 @@ public class SimpleParameterAccessor { return getBindableValue(currentIndex++); } + + + /* + * (non-Javadoc) + * + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() { + + return values.length <= currentIndex; + } + + + /* + * (non-Javadoc) + * + * @see java.util.Iterator#remove() + */ + public void remove() { + + throw new UnsupportedOperationException(); + } } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java index e88043d1c..5633e227c 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java @@ -54,7 +54,8 @@ public interface QueryLookupStrategy { * that can be executed afterwards. * * @param method + * @param domainClass * @return */ - RepositoryQuery resolveQuery(Method method); + RepositoryQuery resolveQuery(Method method, Class domainClass); } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java index 80fa9241b..f05f5e720 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -23,6 +23,8 @@ import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.repository.support.EntityMetadata; +import org.springframework.data.repository.util.ClassUtils; import org.springframework.util.Assert; @@ -35,6 +37,11 @@ import org.springframework.util.Assert; */ public class QueryMethod { + public static enum Type { + + SINGLE_ENTITY, PAGING, COLLECTION, MODIFYING; + } + private final Method method; private final Parameters parameters; @@ -82,27 +89,21 @@ public class QueryMethod { } - /** - * Returns whether the given - * - * @param number - * @return - */ - public boolean isCorrectNumberOfParameters(int number) { + public EntityMetadata getEntityMetadata() { - return number == parameters.getBindableParameters() - .getNumberOfParameters(); + return new EntityMetadata() { + + public Class getJavaType() { + + return getDomainClass(); + } + }; } - /** - * Returns the domain class for this method. - * - * @return - */ - public Class getDomainClass() { + protected Class getDomainClass() { - return getReturnedDomainClass(method); + return ClassUtils.getReturnedDomainClass(method); } @@ -112,7 +113,7 @@ public class QueryMethod { * * @return */ - public boolean isCollectionQuery() { + protected boolean isCollectionQuery() { Class returnType = method.getReturnType(); return org.springframework.util.ClassUtils.isAssignable(List.class, @@ -125,7 +126,7 @@ public class QueryMethod { * * @return */ - public boolean isPageQuery() { + protected boolean isPageQuery() { Class returnType = method.getReturnType(); return org.springframework.util.ClassUtils.isAssignable(Page.class, @@ -133,6 +134,30 @@ public class QueryMethod { } + public Type getType() { + + if (isModifyingQuery()) { + return Type.MODIFYING; + } + + if (isPageQuery()) { + return Type.PAGING; + } + + if (isCollectionQuery()) { + return Type.COLLECTION; + } + + return Type.SINGLE_ENTITY; + } + + + protected boolean isModifyingQuery() { + + return false; + } + + /** * Returns the {@link Parameters} wrapper to gain additional information * about {@link Method} parameters. diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java index 860750b24..69bf0f0f1 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java @@ -15,6 +15,8 @@ */ package org.springframework.data.repository.query; + + /** * Interface for a query abstraction. * @@ -30,4 +32,12 @@ public interface RepositoryQuery { * @return */ public Object execute(Object[] parameters); + + + /** + * Returns the + * + * @return + */ + public QueryMethod getQueryMethod(); } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java index f6997904a..182811375 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2008-2011 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,11 @@ */ package org.springframework.data.repository.query.parser; +import java.util.Iterator; + import org.springframework.data.domain.Sort; -import org.springframework.data.repository.query.SimpleParameterAccessor; -import org.springframework.data.repository.query.SimpleParameterAccessor.BindableParameterIterator; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.parser.PartTree.OrPart; import org.springframework.util.Assert; @@ -32,19 +34,18 @@ import org.springframework.util.Assert; */ public abstract class AbstractQueryCreator { - private final SimpleParameterAccessor parameters; + private final ParameterAccessor parameters; private final PartTree tree; /** * Creates a new {@link AbstractQueryCreator} for the given {@link PartTree} - * and {@link SimpleParameterAccessor}. + * and {@link ParametersParameterAccessor}. * * @param tree * @param parameters */ - public AbstractQueryCreator(PartTree tree, - SimpleParameterAccessor parameters) { + public AbstractQueryCreator(PartTree tree, ParameterAccessor parameters) { Assert.notNull(tree); Assert.notNull(parameters); @@ -61,7 +62,10 @@ public abstract class AbstractQueryCreator { */ public T createQuery() { - return complete(createCriteria(tree), tree.getSort()); + Sort treeSort = tree.getSort(); + Sort sort = treeSort != null ? treeSort : parameters.getSort(); + + return complete(createCriteria(tree), sort); } @@ -75,7 +79,7 @@ public abstract class AbstractQueryCreator { private S createCriteria(PartTree tree) { S base = null; - BindableParameterIterator iterator = parameters.iterator(); + Iterator iterator = parameters.iterator(); for (OrPart node : tree) { @@ -102,7 +106,7 @@ public abstract class AbstractQueryCreator { * @param iterator * @return */ - protected abstract S create(Part part, BindableParameterIterator iterator); + protected abstract S create(Part part, Iterator iterator); /** @@ -114,8 +118,7 @@ public abstract class AbstractQueryCreator { * @param iterator * @return */ - protected abstract S and(Part part, S base, - BindableParameterIterator iterator); + protected abstract S and(Part part, S base, Iterator iterator); /** diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java index e38777ea4..85eb537d8 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java @@ -113,6 +113,18 @@ public class Part { } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return String.format("%s %s", property.getName(), type); + } + + /** * @return the type */ @@ -129,31 +141,34 @@ public class Part { */ public static enum Type { - BETWEEN(null, 2, "Between"), + BETWEEN(2, "Between"), - IS_NOT_NULL(null, 0, "IsNotNull", "NotNull"), + IS_NOT_NULL(0, "IsNotNull", "NotNull"), - IS_NULL(null, 0, "IsNull", "Null"), + IS_NULL(0, "IsNull", "Null"), - LESS_THAN("<", "LessThan"), + LESS_THAN("LessThan"), - GREATER_THAN(">", "GreaterThan"), + GREATER_THAN("GreaterThan"), - NOT_LIKE("not like", "NotLike"), + NOT_LIKE("NotLike"), - LIKE("like", "Like"), + LIKE("Like"), - NEGATING_SIMPLE_PROPERTY("<>", "Not"), + NOT_IN("NotIn"), - SIMPLE_PROPERTY("="); + IN("In"), + + NEGATING_SIMPLE_PROPERTY("Not"), + + SIMPLE_PROPERTY; // Need to list them again explicitly as the order is important // (esp. for IS_NULL, IS_NOT_NULL) private static final List ALL = Arrays.asList(IS_NOT_NULL, IS_NULL, BETWEEN, LESS_THAN, GREATER_THAN, NOT_LIKE, LIKE, - NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY); + NOT_IN, IN, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY); private List keywords; - private String operator; private int numberOfArguments; @@ -166,17 +181,16 @@ public class Part { * @param numberOfArguments * @param keywords */ - private Type(String operator, int numberOfArguments, String... keywords) { + private Type(int numberOfArguments, String... keywords) { - this.operator = operator; this.numberOfArguments = numberOfArguments; this.keywords = Arrays.asList(keywords); } - private Type(String operator, String... keywords) { + private Type(String... keywords) { - this(operator, 1, keywords); + this(1, keywords); } @@ -202,12 +216,6 @@ public class Part { } - public String getOperator() { - - return this.operator; - } - - /** * Returns whether the the type supports the given raw property. Default * implementation checks whether the property ends with the registered diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java index e0e0116ca..a9f644c67 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java @@ -21,11 +21,13 @@ import static java.util.regex.Pattern.*; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.parser.PartTree.OrPart; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** @@ -40,11 +42,13 @@ import org.springframework.util.Assert; public class PartTree implements Iterable { private static final String ORDER_BY = "OrderBy"; - private static final String[] PREFIXES = new String[] { "findBy", "find", - "readBy", "read", "getBy", "get" }; - private static final String PREFIX_TEMPLATE = "^%s(?=[A-Z]).*"; private static final String KEYWORD_TEMPLATE = "(%s)(?=[A-Z])"; + private static final String DISTINCT = "Distinct"; + private static final Pattern PREFIX_TEMPLATE = Pattern + .compile("^(find|read|get)(\\p{Upper}.*?)??By"); + + private final boolean distinct; private final OrderBySource orderBySource; private final List nodes = new ArrayList(); @@ -61,6 +65,7 @@ public class PartTree implements Iterable { Assert.notNull(source); Assert.notNull(domainClass); + this.distinct = detectDistinct(source); String foo = strip(source); String[] parts = split(foo, ORDER_BY); @@ -109,6 +114,37 @@ public class PartTree implements Iterable { } + /** + * Returns whether we indicate distinct lookup of entities. + * + * @return + */ + public boolean isDistinct() { + + return distinct; + } + + + /** + * Returns an {@link Iterable} of all parts contained in the + * {@link PartTree}. + * + * @return + */ + public Iterable getParts() { + + List result = new ArrayList(); + + for (OrPart orPart : this) { + for (Part part : orPart) { + result.add(part); + } + } + + return result; + } + + /** * Splits the given text at the given keywords. Expects camelcase style to * only match concrete keywords and not derivatives of it. @@ -135,15 +171,47 @@ public class PartTree implements Iterable { */ private String strip(String methodName) { - for (String prefix : PREFIXES) { + Matcher matcher = PREFIX_TEMPLATE.matcher(methodName); - String regex = format(PREFIX_TEMPLATE, prefix); - if (methodName.matches(regex)) { - return methodName.substring(prefix.length()); - } + if (matcher.find()) { + return methodName.substring(matcher.group().length()); + } else { + return methodName; + } + } + + + /** + * Checks whether the given source string contains the {@link #DISTINCT} + * keyword in it's prefix. + * + * @param source + * @return + */ + private boolean detectDistinct(String source) { + + Matcher matcher = PREFIX_TEMPLATE.matcher(source); + + if (!matcher.find()) { + return false; } - return methodName; + String group = matcher.group(2); + return group != null && group.contains(DISTINCT); + } + + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return String.format("%s %s", + StringUtils.collectionToDelimitedString(nodes, " or "), + orderBySource.toString()); } /** @@ -176,6 +244,18 @@ public class PartTree implements Iterable { } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return StringUtils.collectionToDelimitedString(children, " and "); + } + + /* * (non-Javadoc) * diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java new file mode 100644 index 000000000..fd715e23e --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java @@ -0,0 +1,70 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +import org.springframework.util.Assert; + + +/** + * Base class for implementations of {@link EntityInformation}. Considers an + * entity to be new whenever {@link #getId(Object)} returns {@literal null}. + * + * @author Oliver Gierke + */ +public abstract class AbstractEntityInformation implements + EntityInformation { + + private final Class domainClass; + + + /** + * Creates a new {@link AbstractEntityInformation} from the given domain + * class. + * + * @param domainClass + */ + public AbstractEntityInformation(Class domainClass) { + + Assert.notNull(domainClass); + this.domainClass = domainClass; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.IsNewAware#isNew(java.lang + * .Object) + */ + public boolean isNew(T entity) { + + return getId(entity) == null; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.EntityInformation#getJavaType + * () + */ + public Class getJavaType() { + + return this.domainClass; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java new file mode 100644 index 000000000..e776bc1ed --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java @@ -0,0 +1,328 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +import static org.springframework.core.GenericTypeResolver.*; +import static org.springframework.data.repository.util.ClassUtils.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.data.repository.Repository; +import org.springframework.util.Assert; + + +/** + * Default implementation of {@link RepositoryMetadata}. + * + * @author Oliver Gierke + */ +public class DefaultRepositoryMetadata implements RepositoryMetadata { + + @SuppressWarnings("rawtypes") + private static final TypeVariable>[] PARAMETERS = + Repository.class.getTypeParameters(); + private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); + private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); + + private final Map methodCache = + new ConcurrentHashMap(); + + private final Class repositoryInterface; + private final Class repositoryBaseClass; + + + /** + * Creates a new {@link DefaultRepositoryMetadata} for the given repository + * interface and repository base class. + * + * @param repositoryInterface + */ + public DefaultRepositoryMetadata(Class repositoryInterface, + Class repositoryBaseClass) { + + Assert.notNull(repositoryInterface); + Assert.notNull(repositoryBaseClass); + this.repositoryInterface = repositoryInterface; + this.repositoryBaseClass = repositoryBaseClass; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * getRepositoryInterface() + */ + public Class getRepositoryInterface() { + + return repositoryInterface; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * getRepositoryBaseClass() + */ + public Class getRepositoryBaseClass() { + + return this.repositoryBaseClass; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.RepositoryMetadata#getDomainClass + * () + */ + public Class getDomainClass() { + + Class[] arguments = + resolveTypeArguments(repositoryInterface, Repository.class); + return arguments == null ? null : arguments[0]; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.RepositoryMetadata#getIdClass + * () + */ + public Class getIdClass() { + + Class[] arguments = + resolveTypeArguments(repositoryInterface, Repository.class); + return arguments == null ? null : arguments[1]; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * getBaseClassMethod(java.lang.reflect.Method) + */ + public Method getBaseClassMethod(Method method) { + + Assert.notNull(method); + + Method result = methodCache.get(method); + + if (null != result) { + return result; + } + + result = getBaseClassMethodFor(method); + methodCache.put(method, result); + + return result; + } + + + /** + * Returns whether the given method is considered to be a repository base + * class method. + * + * @param method + * @return + */ + private boolean isBaseClassMethod(Method method) { + + Assert.notNull(method); + + if (method.getDeclaringClass().isAssignableFrom(repositoryBaseClass)) { + return true; + } + + return !method.equals(getBaseClassMethod(method)); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * getFinderMethods() + */ + public Iterable getQueryMethods() { + + Set result = new HashSet(); + + for (Method method : repositoryInterface.getDeclaredMethods()) { + if (!isCustomMethod(method) && !isBaseClassMethod(method)) { + result.add(method); + } + } + + return result; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.RepositoryMetadata#isCustomMethod + * (java.lang.reflect.Method) + */ + public boolean isCustomMethod(Method method) { + + Class declaringClass = method.getDeclaringClass(); + + boolean isQueryMethod = declaringClass.equals(repositoryInterface); + boolean isRepositoryInterface = + isGenericRepositoryInterface(declaringClass); + boolean isBaseClassMethod = isBaseClassMethod(method); + + return !(isRepositoryInterface || isBaseClassMethod || isQueryMethod); + } + + + /** + * Returns the given base class' method if the given method (declared in the + * repository interface) was also declared at the repository base class. + * Returns the given method if the given base class does not declare the + * method given. Takes generics into account. + * + * @param method + * @return + */ + Method getBaseClassMethodFor(Method method) { + + for (Method baseClassMethod : repositoryBaseClass.getMethods()) { + + // Wrong name + if (!method.getName().equals(baseClassMethod.getName())) { + continue; + } + + // Wrong number of arguments + if (!(method.getParameterTypes().length == baseClassMethod + .getParameterTypes().length)) { + continue; + } + + // Check whether all parameters match + if (!parametersMatch(method, baseClassMethod)) { + continue; + } + + return baseClassMethod; + } + + return method; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * hasCustomMethod() + */ + public boolean hasCustomMethod() { + + // No detection required if no typing interface was configured + if (isGenericRepositoryInterface(repositoryInterface)) { + return false; + } + + for (Method method : repositoryInterface.getMethods()) { + if (isCustomMethod(method) && !isBaseClassMethod(method)) { + return true; + } + } + + return false; + } + + + /** + * Checks the given method's parameters to match the ones of the given base + * class method. Matches generic arguments agains the ones bound in the + * given repository interface. + * + * @param method + * @param baseClassMethod + * @return + */ + private boolean parametersMatch(Method method, Method baseClassMethod) { + + Type[] genericTypes = baseClassMethod.getGenericParameterTypes(); + Class[] types = baseClassMethod.getParameterTypes(); + Class[] methodParameters = method.getParameterTypes(); + + for (int i = 0; i < genericTypes.length; i++) { + + Type type = genericTypes[i]; + + if (type instanceof TypeVariable) { + + String name = ((TypeVariable) type).getName(); + + if (!matchesGenericType(name, methodParameters[i])) { + return false; + } + + } else { + + if (!types[i].equals(methodParameters[i])) { + return false; + } + } + } + + return true; + } + + + /** + * Checks whether the given parameter type matches the generic type of the + * given parameter. Thus when {@literal PK} is declared, the method ensures + * that given method parameter is the primary key type declared in the given + * repository interface e.g. + * + * @param name + * @param parameterType + * @return + */ + private boolean matchesGenericType(String name, Class parameterType) { + + Class entityType = getDomainClass(); + Class idClass = getIdClass(); + + if (ID_TYPE_NAME.equals(name) && parameterType.equals(idClass)) { + return true; + } + + if (DOMAIN_TYPE_NAME.equals(name) && parameterType.equals(entityType)) { + return true; + } + + return false; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java new file mode 100644 index 000000000..33797d33f --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +/** + * Extension of {@link EntityMetadata} to add functionality to query information + * of entity instances. + * + * @author Oliver Gierke + */ +public interface EntityInformation extends EntityMetadata { + + /** + * Returns whether the given entity is considered to be new. + * + * @param entity must never be {@literal null} + * @return + */ + boolean isNew(T entity); + + + /** + * Returns the id of the given entity. + * + * @param entity must never be {@literal null} + * @return + */ + Object getId(T entity); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java similarity index 73% rename from spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java rename to spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java index 02471d509..ffc93e070 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2011 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,16 @@ package org.springframework.data.repository.support; /** - * Interface to abstract the ways to retrieve the id of the given entity. + * Metadata for entity types. * * @author Oliver Gierke */ -public interface IdAware { +public interface EntityMetadata { /** - * Returns the id of the given entity. + * Returns the actual domain class type. * - * @param entity * @return */ - Object getId(Object entity); -} \ No newline at end of file + Class getJavaType(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java index 86f153316..ffb873a39 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2008-201 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,26 @@ import org.springframework.data.domain.Persistable; /** - * Implementation of {@link IsNewAware} that assumes the entity handled + * Implementation of {@link EntityMetadata} that assumes the entity handled * implements {@link Persistable} and uses {@link Persistable#isNew()} for the * {@link #isNew(Object)} check. * * @author Oliver Gierke */ -public class PersistableEntityInformation implements IsNewAware, IdAware { +@SuppressWarnings("rawtypes") +public class PersistableEntityInformation extends + AbstractEntityInformation { + + /** + * Creates a new {@link PersistableEntityInformation}. + * + * @param domainClass + */ + public PersistableEntityInformation(Class domainClass) { + + super(domainClass); + } + /* * (non-Javadoc) @@ -34,9 +47,10 @@ public class PersistableEntityInformation implements IsNewAware, IdAware { * org.springframework.data.repository.support.IsNewAware#isNew(java.lang * .Object) */ - public boolean isNew(Object entity) { + @Override + public boolean isNew(T entity) { - return ((Persistable) entity).isNew(); + return entity.isNew(); } @@ -47,8 +61,8 @@ public class PersistableEntityInformation implements IsNewAware, IdAware { * org.springframework.data.repository.support.IdAware#getId(java.lang.Object * ) */ - public Object getId(Object entity) { + public Object getId(T entity) { - return ((Persistable) entity).getId(); + return entity.getId(); } } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/QueryCreationListener.java similarity index 62% rename from spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java rename to spring-data-commons-core/src/main/java/org/springframework/data/repository/support/QueryCreationListener.java index b2eadec9e..b704dfa78 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/QueryCreationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2011 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,19 +15,21 @@ */ package org.springframework.data.repository.support; +import org.springframework.data.repository.query.RepositoryQuery; + + /** - * Interface to abstract the ways to determine if the given entity is to be - * considered as new. + * Callback for listeners that want to execute functionality on + * {@link RepositoryQuery} creation. * * @author Oliver Gierke */ -public interface IsNewAware { +public interface QueryCreationListener { /** - * Returns whether the given entity is considered to be new. + * Will be invoked just after the {@link RepositoryQuery} was created. * - * @param entity - * @return + * @param query */ - boolean isNew(Object entity); -} \ No newline at end of file + void onCreation(T query); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java deleted file mode 100644 index 4a0a1a3b3..000000000 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2008-2010 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.repository.support; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.ReflectionUtils.FieldCallback; -import org.springframework.util.ReflectionUtils.MethodCallback; - - -/** - * {@link IsNewAware} and {@link IdAware} implementation that reflectively - * checks a {@link Field} or {@link Method} annotated with the given - * annotations. Subclasses usually simply have to provide the persistence - * technology specific annotations. - * - * @author Oliver Gierke - */ -public class ReflectiveEntityInformationSupport implements IsNewAware, IdAware { - - private Field field; - private Method method; - - - /** - * Creates a new {@link ReflectiveEntityInformationSupport} by inspecting - * the given class for a {@link Field} or {@link Method} for and {@link Id} - * annotation. - * - * @param domainClass not {@literal null}, must be annotated with - * {@link Entity} and carry an anootation defining the id - * property. - */ - public ReflectiveEntityInformationSupport(Class domainClass, - final Class... annotationsToScanFor) { - - Assert.notNull(domainClass); - - ReflectionUtils.doWithFields(domainClass, new FieldCallback() { - - public void doWith(Field field) { - - if (ReflectiveEntityInformationSupport.this.field != null) { - return; - } - - if (hasAnnotation(field, annotationsToScanFor)) { - ReflectiveEntityInformationSupport.this.field = field; - } - } - }); - - if (field != null) { - return; - } - - ReflectionUtils.doWithMethods(domainClass, new MethodCallback() { - - public void doWith(Method method) { - - if (ReflectiveEntityInformationSupport.this.method != null) { - return; - } - - if (hasAnnotation(method, annotationsToScanFor)) { - ReflectiveEntityInformationSupport.this.method = method; - } - } - }); - - Assert.isTrue(this.field != null || this.method != null, - "No id method or field found!"); - } - - - /** - * Checks whether the given {@link AnnotatedElement} carries one of the - * given {@link Annotation}s. - * - * @param annotatedElement - * @param annotations - * @return - */ - private boolean hasAnnotation(AnnotatedElement annotatedElement, - Class... annotations) { - - for (Class annotation : annotations) { - - if (annotatedElement.getAnnotation(annotation) != null) { - return true; - } - } - - return false; - } - - - /* - * (non-Javadoc) - * - * @see - * org.springframework.data.repository.support.RepositorySupport.IsNewAware - * #isNew(java.lang.Object) - */ - public boolean isNew(Object entity) { - - return getId(entity) == null; - } - - - /* - * (non-Javadoc) - * - * @see - * org.springframework.data.repository.support.RepositorySupport.IdAware - * #getId(java.lang.Object) - */ - public Object getId(Object entity) { - - if (field != null) { - ReflectionUtils.makeAccessible(field); - return ReflectionUtils.getField(field, entity); - } - - ReflectionUtils.makeAccessible(method); - return ReflectionUtils.invokeMethod(method, entity); - } -} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java index 3fe899c8d..00ab4b49b 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2008-2011 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,6 @@ */ package org.springframework.data.repository.support; -import java.util.Collections; -import java.util.List; - import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Required; @@ -124,24 +121,6 @@ public abstract class RepositoryFactoryBeanSupport> this.factory = createRepositoryFactory(); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); - this.factory.validate(repositoryInterface, customImplementation); - - for (RepositoryProxyPostProcessor processor : getRepositoryPostProcessors()) { - this.factory.addRepositoryProxyPostProcessor(processor); - } - } - - - /** - * Returns all {@link RepositoryProxyPostProcessor} to be added to the - * repository factory to be created. Default implementation will return an - * empty list. - * - * @return - */ - protected List getRepositoryPostProcessors() { - - return Collections.emptyList(); } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java index 7f20fdf3a..ecb978219 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java @@ -15,21 +15,18 @@ */ package org.springframework.data.repository.support; -import static org.springframework.data.repository.util.ClassUtils.*; import static org.springframework.util.ReflectionUtils.*; -import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.ProxyFactory; +import org.springframework.core.GenericTypeResolver; import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; @@ -50,12 +47,11 @@ import org.springframework.util.Assert; */ public abstract class RepositoryFactorySupport { - private QueryLookupStrategy.Key queryLookupStrategyKey; - - private final Map methodCache = - new ConcurrentHashMap(); private final List postProcessors = new ArrayList(); + private QueryLookupStrategy.Key queryLookupStrategyKey; + private List> queryPostProcessors = + new ArrayList>(); /** @@ -69,6 +65,20 @@ public abstract class RepositoryFactorySupport { } + /** + * Adds a {@link QueryCreationListener} to the factory to plug in + * functionality triggered right after creation of {@link RepositoryQuery} + * instances. + * + * @param listener + */ + public void addQueryCreationListener(QueryCreationListener listener) { + + Assert.notNull(listener); + this.queryPostProcessors.add(listener); + } + + /** * Adds {@link RepositoryProxyPostProcessor}s to the factory to allow * manipulation of the {@link ProxyFactory} before the proxy gets created. @@ -78,7 +88,7 @@ public abstract class RepositoryFactorySupport { * * @param processor */ - protected void addRepositoryProxyPostProcessor( + public void addRepositoryProxyPostProcessor( RepositoryProxyPostProcessor processor) { Assert.notNull(processor); @@ -109,15 +119,17 @@ public abstract class RepositoryFactorySupport { * @param customImplementation * @return */ - @SuppressWarnings("unchecked") - public > T getRepository( - Class repositoryInterface, Object customImplementation) { + @SuppressWarnings({ "unchecked" }) + public T getRepository(Class repositoryInterface, + Object customImplementation) { - validate(repositoryInterface, customImplementation); + RepositoryMetadata metadata = + new DefaultRepositoryMetadata(repositoryInterface, + getRepositoryBaseClass(repositoryInterface)); - Class domainClass = getDomainClass(repositoryInterface); - RepositorySupport target = - getTargetRepository(domainClass, repositoryInterface); + validate(metadata, customImplementation); + + Object target = getTargetRepository(metadata); // Create proxy ProxyFactory result = new ProxyFactory(); @@ -128,32 +140,31 @@ public abstract class RepositoryFactorySupport { processor.postProcess(result); } - result.addAdvice(new QueryExecuterMethodInterceptor( - repositoryInterface, customImplementation, target)); + result.addAdvice(new QueryExecuterMethodInterceptor(metadata, + customImplementation, target)); return (T) result.getProxy(); } /** - * Create a {@link RepositorySupport} instance as backing for the query - * proxy. + * Create a repository instance as backing for the query proxy. * - * @param * @param domainClass * @return */ - protected abstract RepositorySupport getTargetRepository( - Class domainClass, Class repositoryInterface); + protected abstract Object getTargetRepository(RepositoryMetadata metadata); /** - * Determines the base class for the repository to be created. + * Returns the base class backing the actual repository instance. Make sure + * {@link #getTargetRepository(RepositoryMetadata)} returns an instance of + * this class. * + * @param repositoryInterface * @return */ - @SuppressWarnings("rawtypes") - protected abstract Class getRepositoryClass( + protected abstract Class getRepositoryBaseClass( Class repositoryInterface); @@ -166,165 +177,23 @@ public abstract class RepositoryFactorySupport { protected abstract QueryLookupStrategy getQueryLookupStrategy(Key key); - /** - * Returns if the configured repository interface has custom methods, that - * might have to be delegated to a custom implementation. This is used to - * verify repository configuration. - * - * @return - */ - private boolean hasCustomMethod( - Class> repositoryInterface) { - - boolean hasCustomMethod = false; - - // No detection required if no typing interface was configured - if (isGenericRepositoryInterface(repositoryInterface)) { - return false; - } - - for (Method method : repositoryInterface.getMethods()) { - - if (isCustomMethod(method, repositoryInterface) - && !isBaseClassMethod(method, repositoryInterface)) { - return true; - } - } - - return hasCustomMethod; - } - - - /** - * Returns whether the given method is considered to be a repository base - * class method. - * - * @param method - * @param repositoryInterface - * @return - */ - private boolean isBaseClassMethod(Method method, - Class repositoryInterface) { - - Assert.notNull(method); - - if (method.getDeclaringClass().isAssignableFrom( - getRepositoryClass(repositoryInterface))) { - return true; - } - - return !method.equals(getBaseClassMethod(method, repositoryInterface)); - } - - - /** - * Returns the base class method that is backing the given method. This can - * be necessary if a repository interface redeclares a method in - * {@link Repository} (e.g. for transaction behaviour customization). - * Returns the method itself if the base class does not implement the given - * method. - * - * @param method - * @return - */ - private Method getBaseClassMethod(Method method, - Class repositoryInterface) { - - Assert.notNull(method); - - Method result = methodCache.get(method); - - if (null != result) { - return result; - } - - result = - getBaseClassMethodFor(method, - getRepositoryClass(repositoryInterface), - repositoryInterface); - methodCache.put(method, result); - - return result; - } - - - /** - * Returns whether the given method is a custom repository method. - * - * @param method - * @param repositoryInterface - * @return - */ - private boolean isCustomMethod(Method method, Class repositoryInterface) { - - Class declaringClass = method.getDeclaringClass(); - - boolean isQueryMethod = declaringClass.equals(repositoryInterface); - boolean isRepositoryInterface = - isGenericRepositoryInterface(declaringClass); - boolean isBaseClassMethod = - isBaseClassMethod(method, repositoryInterface); - - return !(isRepositoryInterface || isBaseClassMethod || isQueryMethod); - } - - - /** - * Returns all methods considered to be finder methods. - * - * @param repositoryInterface - * @return - */ - private Iterable getFinderMethods(Class repositoryInterface) { - - Set result = new HashSet(); - - for (Method method : repositoryInterface.getDeclaredMethods()) { - if (!isCustomMethod(method, repositoryInterface) - && !isBaseClassMethod(method, repositoryInterface)) { - result.add(method); - } - } - - return result; - } - - - /** - * Validates the given repository interface. - * - * @param repositoryInterface - */ - private void validate(Class repositoryInterface) { - - Assert.notNull(repositoryInterface); - Assert.notNull( - getDomainClass(repositoryInterface), - "Could not retrieve domain class from interface. Make sure it extends GenericRepository."); - - } - - /** * Validates the given repository interface as well as the given custom * implementation. * - * @param repositoryInterface + * @param repositoryMetadata * @param customImplementation */ - protected void validate( - Class> repositoryInterface, + protected void validate(RepositoryMetadata repositoryMetadata, Object customImplementation) { - validate(repositoryInterface); - if (null == customImplementation - && hasCustomMethod(repositoryInterface)) { + && repositoryMetadata.hasCustomMethod()) { throw new IllegalArgumentException( String.format( "You have custom methods in %s but not provided a custom implementation!", - repositoryInterface)); + repositoryMetadata.getRepositoryInterface())); } } @@ -343,8 +212,8 @@ public abstract class RepositoryFactorySupport { new ConcurrentHashMap(); private final Object customImplementation; - private final Class repositoryInterface; - private final RepositorySupport target; + private final RepositoryMetadata metadata; + private final Object target; /** @@ -352,19 +221,40 @@ public abstract class RepositoryFactorySupport { * of {@link QueryMethod}s to be invoked on execution of repository * interface methods. */ - public QueryExecuterMethodInterceptor(Class repositoryInterface, - Object customImplementation, RepositorySupport target) { + public QueryExecuterMethodInterceptor( + RepositoryMetadata repositoryMetadata, + Object customImplementation, Object target) { - this.repositoryInterface = repositoryInterface; + this.metadata = repositoryMetadata; this.customImplementation = customImplementation; this.target = target; QueryLookupStrategy lookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey); - for (Method method : getFinderMethods(repositoryInterface)) { + for (Method method : metadata.getQueryMethods()) { + RepositoryQuery query = + lookupStrategy.resolveQuery(method, + repositoryMetadata.getDomainClass()); + invokeListeners(query, metadata); + queries.put(method, query); + } + } - queries.put(method, lookupStrategy.resolveQuery(method)); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void invokeListeners(RepositoryQuery query, + RepositoryMetadata metadata) { + + for (QueryCreationListener listener : queryPostProcessors) { + Class typeArgument = + GenericTypeResolver.resolveTypeArgument( + listener.getClass(), + QueryCreationListener.class); + if (typeArgument != null + && typeArgument.isAssignableFrom(query.getClass())) { + listener.onCreation(query); + } } } @@ -393,8 +283,7 @@ public abstract class RepositoryFactorySupport { // Lookup actual method as it might be redeclared in the interface // and we have to use the repository instance nevertheless - Method actualMethod = - getBaseClassMethod(method, repositoryInterface); + Method actualMethod = metadata.getBaseClassMethod(method); return executeMethodOn(target, actualMethod, invocation.getArguments()); } @@ -449,7 +338,7 @@ public abstract class RepositoryFactorySupport { return false; } - return isCustomMethod(invocation.getMethod(), repositoryInterface); + return metadata.isCustomMethod(invocation.getMethod()); } } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryMetadata.java new file mode 100644 index 000000000..9d75b7862 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryMetadata.java @@ -0,0 +1,104 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +import java.lang.reflect.Method; + + +/** + * Metadata for repository interfaces. + * + * @author Oliver Gierke + */ +public interface RepositoryMetadata { + + /** + * Returns the repository interface. + * + * @return + */ + Class getRepositoryInterface(); + + + /** + * Returns the base class to be used to create the proxy backing instance. + * + * @return + */ + Class getRepositoryBaseClass(); + + + /** + * Returns if the configured repository interface has custom methods, that + * might have to be delegated to a custom implementation. This is used to + * verify repository configuration. + * + * @return + */ + boolean hasCustomMethod(); + + + /** + * Returns whether the given method is a custom repository method. + * + * @param method + * @param baseClass + * @return + */ + boolean isCustomMethod(Method method); + + + /** + * Returns all methods considered to be query methods. + * + * @param repositoryInterface + * @return + */ + Iterable getQueryMethods(); + + + /** + * Returns the base class method that is backing the given method. This can + * be necessary if a repository interface redeclares a method of the core + * repository interface (e.g. for transaction behaviour customization). + * Returns the method itself if the base class does not implement the given + * method. + * + * @param method + * @return + */ + Method getBaseClassMethod(Method method); + + + /** + * Returns the domain class the repository is declared for. + * + * @param clazz + * @return the domain class the repository is handling or {@code null} if + * none found. + */ + Class getDomainClass(); + + + /** + * Returns the id class the given class is declared for. + * + * @param clazz + * @return the id class of the entity managed by the repository for or + * {@code null} if none found. + */ + Class getIdClass(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java deleted file mode 100644 index 1def33e85..000000000 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2008-2010 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.repository.support; - -import java.io.Serializable; - -import org.springframework.data.domain.Persistable; -import org.springframework.data.repository.Repository; -import org.springframework.util.Assert; - - -/** - * Abstract base class for generic repositories. Captures information about the - * domain class to be managed. - * - * @author Oliver Gierke - * @param the type of entity to be handled - */ -public abstract class RepositorySupport implements - Repository { - - private final Class domainClass; - private final IsNewAware isNewStrategy; - - - /** - * Creates a new {@link RepositorySupport}. - * - * @param domainClass - */ - public RepositorySupport(Class domainClass) { - - Assert.notNull(domainClass); - this.domainClass = domainClass; - this.isNewStrategy = createIsNewStrategy(domainClass); - Assert.notNull(isNewStrategy); - } - - - /** - * Returns the domain class to handle. - * - * @return the domain class - */ - protected Class getDomainClass() { - - return domainClass; - } - - - /** - * Return whether the given entity is to be regarded as new. Default - * implementation will inspect the given domain class and use either - * {@link PersistableEntityInformation} if the class implements - * {@link Persistable} or {@link ReflectiveEntityInformation} otherwise. - * - * @param entity - * @return - */ - protected abstract IsNewAware createIsNewStrategy(Class domainClass); - - - /** - * Returns the strategy how to determine whether an entity is to be regarded - * as new. - * - * @return the isNewStrategy - */ - protected IsNewAware getIsNewStrategy() { - - return isNewStrategy; - } -} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java index 4f2df71b2..4a189163f 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java @@ -15,9 +15,6 @@ */ package org.springframework.data.repository.support; -import java.util.Arrays; -import java.util.List; - import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.ListableBeanFactory; @@ -59,20 +56,32 @@ public abstract class TransactionalRepositoryFactoryBeanSupport getRepositoryPostProcessors() { + protected final RepositoryFactorySupport createRepositoryFactory() { - return Arrays.asList(txPostProcessor); + RepositoryFactorySupport factory = doCreateRepositoryFactory(); + factory.addRepositoryProxyPostProcessor(txPostProcessor); + return factory; } + /** + * Creates the actual {@link RepositoryFactorySupport} instance. + * + * @return + */ + protected abstract RepositoryFactorySupport doCreateRepositoryFactory(); + + /* * (non-Javadoc) * diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java index 203a4eb5c..ef41bee9c 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java @@ -15,17 +15,14 @@ */ package org.springframework.data.repository.util; -import static org.springframework.core.GenericTypeResolver.*; - -import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; import java.util.Arrays; import java.util.Collection; +import org.springframework.data.domain.Page; import org.springframework.data.repository.Repository; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -38,13 +35,6 @@ import org.springframework.util.StringUtils; */ public abstract class ClassUtils { - @SuppressWarnings("rawtypes") - private static final TypeVariable>[] PARAMETERS = - Repository.class.getTypeParameters(); - private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); - private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); - - /** * Private constructor to prevent instantiation. */ @@ -53,40 +43,6 @@ public abstract class ClassUtils { } - /** - * Returns the domain class the given class is declared for. Will introspect - * the given class for extensions of {@link Repository} and retrieve the - * domain class type from its generics declaration. - * - * @param clazz - * @return the domain class the given class is repository for or - * {@code null} if none found. - */ - public static Class getDomainClass(Class clazz) { - - Class[] arguments = resolveTypeArguments(clazz, Repository.class); - return arguments == null ? null : arguments[0]; - } - - - /** - * Returns the id class the given class is declared for. Will introspect the - * given class for extensions of {@link Repository} or and retrieve the - * {@link Serializable} type from its generics declaration. - * - * @param clazz - * @return the id class the given class is repository for or {@code null} if - * none found. - */ - @SuppressWarnings("unchecked") - public static Class getIdClass(Class clazz) { - - Class[] arguments = resolveTypeArguments(clazz, Repository.class); - return (Class) (arguments == null ? null - : arguments[1]); - } - - /** * Returns the domain class returned by the given {@link Method}. Will * extract the type from {@link Collection}s and @@ -97,15 +53,18 @@ public abstract class ClassUtils { */ public static Class getReturnedDomainClass(Method method) { - Type type = method.getGenericReturnType(); + Class returnType = method.getReturnType(); + + if (Collection.class.isAssignableFrom(returnType) + || Page.class.isAssignableFrom(returnType)) { + + Type type = method.getGenericReturnType(); - if (type instanceof ParameterizedType) { return (Class) ((ParameterizedType) type) .getActualTypeArguments()[0]; - - } else { - return method.getReturnType(); } + + return returnType; } @@ -241,114 +200,4 @@ public abstract class ClassUtils { throw ex; } - - - /** - * Returns the given base class' method if the given method (declared in the - * interface) was also declared at the base class. Returns the given method - * if the given base class does not declare the method given. Takes generics - * into account. - * - * @param method - * @param baseClass - * @param repositoryInterface - * @return - */ - public static Method getBaseClassMethodFor(Method method, - Class baseClass, Class repositoryInterface) { - - for (Method baseClassMethod : baseClass.getMethods()) { - - // Wrong name - if (!method.getName().equals(baseClassMethod.getName())) { - continue; - } - - // Wrong number of arguments - if (!(method.getParameterTypes().length == baseClassMethod - .getParameterTypes().length)) { - continue; - } - - // Check whether all parameters match - if (!parametersMatch(method, baseClassMethod, repositoryInterface)) { - continue; - } - - return baseClassMethod; - } - - return method; - } - - - /** - * Checks the given method's parameters to match the ones of the given base - * class method. Matches generic arguments agains the ones bound in the - * given repository interface. - * - * @param method - * @param baseClassMethod - * @param repositoryInterface - * @return - */ - private static boolean parametersMatch(Method method, - Method baseClassMethod, Class repositoryInterface) { - - Type[] genericTypes = baseClassMethod.getGenericParameterTypes(); - Class[] types = baseClassMethod.getParameterTypes(); - Class[] methodParameters = method.getParameterTypes(); - - for (int i = 0; i < genericTypes.length; i++) { - - Type type = genericTypes[i]; - - if (type instanceof TypeVariable) { - - String name = ((TypeVariable) type).getName(); - - if (!matchesGenericType(name, methodParameters[i], - repositoryInterface)) { - return false; - } - - } else { - - if (!types[i].equals(methodParameters[i])) { - return false; - } - } - } - - return true; - } - - - /** - * Checks whether the given parameter type matches the generic type of the - * given parameter. Thus when {@literal PK} is declared, the method ensures - * that given method parameter is the primary key type declared in the given - * repository interface e.g. - * - * @param name - * @param parameterType - * @param repositoryInterface - * @return - */ - private static boolean matchesGenericType(String name, - Class parameterType, Class repositoryInterface) { - - Class entityType = getDomainClass(repositoryInterface); - Class idClass = getIdClass(repositoryInterface); - - if (ID_TYPE_NAME.equals(name) && parameterType.equals(idClass)) { - return true; - } - - if (DOMAIN_TYPE_NAME.equals(name) && parameterType.equals(entityType)) { - return true; - } - - return false; - } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java b/spring-data-commons-core/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java index 6747a900b..795d7c059 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java +++ b/spring-data-commons-core/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java @@ -44,8 +44,13 @@ public abstract class AbstractConstructorEntityInstantiator iterator = tree.iterator(); diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java new file mode 100644 index 000000000..9b114f0b2 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + + +/** + * Unit tests for {@link AbstractEntityInformation}. + * + * @author Oliver Gierke + */ +public class AbstractEntityInformationUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullDomainClass() throws Exception { + + new DummyAbstractEntityInformation(null); + } + + + @Test + public void considersEntityNewIfGetIdReturnsNull() throws Exception { + + EntityInformation metadata = + new DummyAbstractEntityInformation(Object.class); + assertThat(metadata.isNew(null), is(true)); + assertThat(metadata.isNew(new Object()), is(false)); + } + + private static class DummyAbstractEntityInformation extends + AbstractEntityInformation { + + public DummyAbstractEntityInformation(Class domainClass) { + + super(domainClass); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.EntityMetadata#getId( + * java.lang.Object) + */ + public Object getId(Object entity) { + + return entity == null ? null : entity.toString(); + } + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java new file mode 100644 index 000000000..7eb6da8d0 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java @@ -0,0 +1,187 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.util.ClassUtils; + + +/** + * Unit tests for {@link DefaultRepositoryMetadata}. + * + * @author Oliver Gierke + */ +public class DefaultRepositoryMetadataUnitTests { + + @SuppressWarnings("rawtypes") + static final Class REPOSITORY = + DummyGenericRepositorySupport.class; + + + @Test + public void looksUpDomainClassCorrectly() throws Exception { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata(UserRepository.class, REPOSITORY); + assertEquals(User.class, metadata.getDomainClass()); + + metadata = new DefaultRepositoryMetadata(SomeDao.class, REPOSITORY); + assertEquals(User.class, metadata.getDomainClass()); + } + + + @Test + public void findsDomainClassOnExtensionOfDaoInterface() throws Exception { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata( + ExtensionOfUserCustomExtendedDao.class, REPOSITORY); + assertEquals(User.class, metadata.getDomainClass()); + } + + + @Test + public void detectsParameterizedEntitiesCorrectly() { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata(GenericEntityRepository.class, + REPOSITORY); + assertEquals(GenericEntity.class, metadata.getDomainClass()); + } + + + @Test + public void looksUpIdClassCorrectly() throws Exception { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata(UserRepository.class, REPOSITORY); + + assertEquals(Integer.class, metadata.getIdClass()); + } + + + @Test + public void discoversRepositoryBaseClassMethod() throws Exception { + + Method method = FooDao.class.getMethod("findById", Integer.class); + DefaultRepositoryMetadata metadata = + new DefaultRepositoryMetadata(FooDao.class, REPOSITORY); + + Method reference = metadata.getBaseClassMethodFor(method); + assertEquals(REPOSITORY, reference.getDeclaringClass()); + assertThat(reference.getName(), is("findById")); + } + + + @Test + public void discoveresNonRepositoryBaseClassMethod() throws Exception { + + Method method = FooDao.class.getMethod("readById", Long.class); + + DefaultRepositoryMetadata metadata = + new DefaultRepositoryMetadata(FooDao.class, Repository.class); + + assertThat(metadata.getBaseClassMethodFor(method), is(method)); + } + + @SuppressWarnings("unused") + private class User { + + private String firstname; + + + public String getAddress() { + + return null; + } + } + + static interface UserRepository extends Repository { + + } + + /** + * Sample interface to serve two purposes: + *
    + *
  1. Check that {@link ClassUtils#getDomainClass(Class)} skips non + * {@link GenericDao} interfaces
  2. + *
  3. Check that {@link ClassUtils#getDomainClass(Class)} traverses + * interface hierarchy
  4. + *
+ * + * @author Oliver Gierke + */ + private interface SomeDao extends Serializable, UserRepository { + + Page findByFirstname(Pageable pageable, String firstname); + } + + private static interface FooDao extends Repository { + + // Redeclared method + User findById(Integer primaryKey); + + + // Not a redeclared method + User readById(Long primaryKey); + } + + /** + * Sample interface to test recursive lookup of domain class. + * + * @author Oliver Gierke + */ + static interface ExtensionOfUserCustomExtendedDao extends + UserCustomExtendedRepository { + + } + + static interface UserCustomExtendedRepository extends + Repository { + + } + + static abstract class DummyGenericRepositorySupport + implements Repository { + + public T findById(ID id) { + + return null; + } + } + + /** + * Helper class to reproduce #256. + * + * @author Oliver Gierke + */ + static class GenericEntity { + } + + static interface GenericEntityRepository extends + Repository, Long> { + + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java deleted file mode 100644 index 16fdc8dd7..000000000 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2008-2010 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.repository.support; - -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; - -import org.junit.Test; -import org.springframework.data.domain.Persistable; - - -/** - * Unit test for {@link PersistableEntityInformation}. - * - * @author Oliver Gierke - */ -public class PersistableEntityInformationTests { - - @Test - public void detectsPersistableCorrectly() throws Exception { - - PersistableEntityInformation info = new PersistableEntityInformation(); - - assertNewAndNoId(info, new PersistableEntity(null)); - assertNotNewAndId(info, new PersistableEntity(1L), 1L); - } - - - private void assertNewAndNoId(T info, - Object entity) { - - assertThat(info.isNew(entity), is(true)); - assertThat(info.getId(entity), is(nullValue())); - } - - - private void assertNotNewAndId(T info, - Object entity, Object id) { - - assertThat(info.isNew(entity), is(false)); - assertThat(info.getId(entity), is(id)); - } - - static class PersistableEntity implements Persistable { - - private static final long serialVersionUID = -5898780128204716452L; - - private final Long id; - - - public PersistableEntity(Long id) { - - this.id = id; - } - - - public Long getId() { - - return id; - } - - - public boolean isNew() { - - return id == null; - } - } -} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java new file mode 100644 index 000000000..e14b917fd --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.domain.Persistable; + + +/** + * Unit tests for {@link PersistableEntityMetadata}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class PersistableEntityInformationUnitTests { + + @SuppressWarnings("rawtypes") + static final PersistableEntityInformation metadata = + new PersistableEntityInformation(Persistable.class); + + @Mock + Persistable persistable; + + + @Test + public void usesPersistablesGetId() throws Exception { + + when(persistable.getId()).thenReturn(2L, 1L, 3L); + assertEquals(2L, metadata.getId(persistable)); + assertEquals(1L, metadata.getId(persistable)); + assertEquals(3L, metadata.getId(persistable)); + } + + + @Test + public void usesPersistablesIsNew() throws Exception { + + when(persistable.isNew()).thenReturn(true, false); + assertThat(metadata.isNew(persistable), is(true)); + assertThat(metadata.isNew(persistable), is(false)); + } + + + @Test + public void returnsGivenClassAsEntityType() throws Exception { + + PersistableEntityInformation info = + new PersistableEntityInformation( + PersistableEntity.class); + + assertEquals(PersistableEntity.class, info.getJavaType()); + } + + @SuppressWarnings("serial") + static class PersistableEntity implements Persistable { + + public Long getId() { + + return null; + } + + + public boolean isNew() { + + return false; + } + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java new file mode 100644 index 000000000..8043fe941 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.support; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.RepositoryQuery; + + +/** + * Unit tests for {@link RepositoryFactorySupport}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class RepositoryFactorySupportUnitTests { + + RepositoryFactorySupport factory = new DummyRepositoryFactory(); + + @Mock + MyQueryCreationListener listener; + @Mock + PlainQueryCreationListener otherListener; + + + @Test + public void invokesCustomQueryCreationListenerForSpecialRepositoryQueryOnly() + throws Exception { + + factory.addQueryCreationListener(listener); + factory.addQueryCreationListener(otherListener); + + factory.getRepository(ObjectRepository.class); + + verify(listener, times(1)).onCreation(any(MyRepositoryQuery.class)); + verify(otherListener, times(2)).onCreation(any(RepositoryQuery.class)); + + } + + class DummyRepositoryFactory extends RepositoryFactorySupport { + + @Override + protected Object getTargetRepository(RepositoryMetadata metadata) { + + return new Object(); + } + + + @Override + protected Class getRepositoryBaseClass(Class repositoryInterface) { + + return Object.class; + } + + + @Override + protected QueryLookupStrategy getQueryLookupStrategy(Key key) { + + MyRepositoryQuery queryOne = mock(MyRepositoryQuery.class); + RepositoryQuery queryTwo = mock(RepositoryQuery.class); + + QueryLookupStrategy strategy = mock(QueryLookupStrategy.class); + when(strategy.resolveQuery(any(Method.class), any(Class.class))) + .thenReturn(queryOne, queryTwo); + + return strategy; + } + } + + interface ObjectRepository extends Repository { + + Object findByClass(Class clazz); + + + Object findByFoo(); + } + + interface PlainQueryCreationListener extends + QueryCreationListener { + + } + + interface MyQueryCreationListener extends + QueryCreationListener { + + } + + interface MyRepositoryQuery extends RepositoryQuery { + + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java index 8f7c9989f..98df4b23b 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java @@ -15,18 +15,15 @@ */ package org.springframework.data.repository.util; -import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.data.repository.util.ClassUtils.*; import java.io.Serializable; -import java.lang.reflect.Method; import org.junit.Test; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.support.RepositorySupport; /** @@ -36,23 +33,6 @@ import org.springframework.data.repository.support.RepositorySupport; */ public class ClassUtilsUnitTests { - @Test - public void looksUpDomainClassCorrectly() throws Exception { - - assertEquals(User.class, getDomainClass(UserRepository.class)); - assertEquals(User.class, getDomainClass(SomeDao.class)); - assertNull(getDomainClass(Serializable.class)); - } - - - @Test - public void looksUpIdClassCorrectly() throws Exception { - - assertEquals(Integer.class, getIdClass(UserRepository.class)); - assertNull(getIdClass(Serializable.class)); - } - - @Test(expected = IllegalStateException.class) public void rejectsInvalidReturnType() throws Exception { @@ -62,10 +42,13 @@ public class ClassUtilsUnitTests { @Test - public void findsDomainClassOnExtensionOfDaoInterface() throws Exception { + public void determinesReturnType() throws Exception { assertEquals(User.class, - getDomainClass(ExtensionOfUserCustomExtendedDao.class)); + getReturnedDomainClass(SomeDao.class.getMethod( + "findByFirstname", Pageable.class, String.class))); + assertEquals(GenericType.class, + getReturnedDomainClass(SomeDao.class.getMethod("someMethod"))); } @@ -77,48 +60,6 @@ public class ClassUtilsUnitTests { assertFalse(hasProperty(User.class, "address")); } - - /** - * References #256. - */ - @Test - public void detectsParameterizedEntitiesCorrectly() { - - assertEquals(GenericEntity.class, - getDomainClass(GenericEntityDao.class)); - } - - - /** - * #301 - */ - @Test - public void discoversDaoBaseClassMethod() throws Exception { - - Method method = FooDao.class.getMethod("findById", Integer.class); - - Method reference = - getBaseClassMethodFor(method, - DummyGenericRepositorySupport.class, FooDao.class); - assertEquals(DummyGenericRepositorySupport.class, - reference.getDeclaringClass()); - assertThat(reference.getName(), is("findById")); - } - - - /** - * #301 - */ - @Test - public void discoveresNonDaoBaseClassMethod() throws Exception { - - Method method = FooDao.class.getMethod("readById", Long.class); - - assertThat( - getBaseClassMethodFor(method, RepositorySupport.class, - FooDao.class), is(method)); - } - @SuppressWarnings("unused") private class User { @@ -149,63 +90,12 @@ public class ClassUtilsUnitTests { private interface SomeDao extends Serializable, UserRepository { Page findByFirstname(Pageable pageable, String firstname); + + + GenericType someMethod(); } - /** - * Sample interface to test recursive lookup of domain class. - * - * @author Oliver Gierke - */ - static interface ExtensionOfUserCustomExtendedDao extends - UserCustomExtendedRepository { + private class GenericType { } - - static interface UserCustomExtendedRepository extends - Repository { - - } - - /** - * Helper class to reproduce #256. - * - * @author Oliver Gierke - */ - static class GenericEntity { - } - - static interface GenericEntityDao extends - Repository, Long> { - - } - - /** - * Sample DAO interface to test redeclaration of {@link GenericDao} methods. - * - * @author Oliver Gierke - */ - private static interface FooDao extends Repository { - - // Redeclared method - User findById(Integer primaryKey); - - - // Not a redeclared method - User readById(Long primaryKey); - } - - static abstract class DummyGenericRepositorySupport - extends RepositorySupport { - - public DummyGenericRepositorySupport(Class domainClass) { - - super(domainClass); - } - - - public T findById(ID id) { - - return null; - } - } } diff --git a/spring-data-commons-parent/pom.xml b/spring-data-commons-parent/pom.xml index fc508c01d..af749539a 100644 --- a/spring-data-commons-parent/pom.xml +++ b/spring-data-commons-parent/pom.xml @@ -223,7 +223,7 @@ --> org.springframework.build.aws org.springframework.build.aws.maven - 2.0.0.RELEASE + 3.1.0.RELEASE @@ -269,8 +269,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.5 - 1.5 + 1.6 + 1.6 -Xlint:all true false diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 91dbfe83a..be90e9b5b 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -1,6 +1,16 @@ Spring Data Commons Changelog ============================================= +Changes in version 1.0.0.M4 +---------------------------------------- + +Repository +* Improved ParameterAccessor infrastructure +* Added support for 'Distinct' keyword in finder method names (DATACMNS-15) +* Added support for 'In' and 'NotIn' keywords (DATACMNS-16) +* Introduced metamodel for entities and repositories (DATACMNS-17) +* Fixed returning wrong class PersistableEntityMetadata(DATACMNS-19) + Changes in version 1.0.0.M3 (2011-02-09) ----------------------------------------