Documentation on handling events. Also added support for Spring Data Commons 1.3.0.RC2, which has improved metadata handling.

This commit is contained in:
Jon Brisbin
2012-05-02 16:24:53 -05:00
parent 57861b5536
commit 749382dad7
18 changed files with 191 additions and 99 deletions

View File

@@ -11,9 +11,11 @@ allprojects {
exclude group: "commons-logging"
exclude module: "slf4j-log4j12"
exclude module: "groovy-all", version: "1.8.0-beta-3-SNAPSHOT"
resolutionStrategy.cacheChangingModulesFor(0, "seconds")
}
repositories {
maven { url "http://repo.springsource.org/libs-snapshot" }
maven { url "http://repo.springsource.org/libs-milestone" }
maven { url "http://repo.springsource.org/libs-release" }
}

111
doc/handling_events.md Normal file
View File

@@ -0,0 +1,111 @@
# Handling ApplicationEvents in the REST Exporter
There are six different events that the REST exporter emits throughout the process of working with an entity. Those are:
* BeforeSaveEvent
* AfterSaveEvent
* BeforeLinkSaveEvent
* AfterLinkSaveEvent
* BeforeDeleteEvent
* AfterDeleteEvent
### ApplicationListener
There is an abstract class you can subclass which listens for these kinds of events and calls
the appropriate method based on the event type. You just override the methods for
the events you're interested in.
public class BeforeSaveEventListener extends AbstractRepositoryEventListener {
@Override public void onBeforeSave(Object entity) {
... logic to handle inspecting the entity before the Repository saves it
}
@Override public void onAfterDelete(Object entity) {
... send a message that this entity has been deleted
}
}
One thing to note with this approach, however, is that it makes no distinction based on
the type of the entity. You'll have to inspect that yourself.
### Annotated Handler
Another approach is to use an annotated handler, which does filter events based on domain type.
To declare a handler, create a POJO and put the `@RepositoryEventHandler` annotation on it.
This tells the classpath scanner that this class needs to be inspected for handler methods.
Once it finds a class with this annotation, it iterates over the exposed methods and looks for
annotations that correspond to the event you're interested in. For example, to handle BeforeSaveEvents
in an annotated POJO for different kinds of domain types, you'd define your class like this:
@RepositoryEventHandler
public class PersonEventHandler {
@HandleBeforeSave(Person.class) public void handlePersonSave(Person p) {
... you can now deal with Person in a type-safe way
}
@HandleBeforeSave(Profile.class) public void handleProfileSave(Profile p) {
... you can now deal with Profile in a type-safe way
}
}
You can also declare the domain type at the class level:
@RepositoryEventHandler(Person.class)
public class PersonEventHandler {
@HandleBeforeSave public void handleBeforeSave(Person p) {
...
}
@HandleAfterDelete public void handleAfterDelete(Person p) {
...
}
}
To actually get your handler invoked, however, you need to declare an instance of it in your
ApplicationContext. The classpath scanner will look for event handlers and build up information
about them, but it won't actually wire a handler to accept events unless there's an instance of
it declared in your ApplicationContext.
(In JavaConfig style):
@Configuration
public class RepositoryConfiguration {
@Bean PersonEventHandler personEventHandler() {
return new PersonEventHandler();
}
}
When you have your beans properly declared, you need to declare an instance of the ApplicationListener.
You can pass the base package of the packages you want searched for handlers in the constructor.
@Configuration
public class RepositoryConfiguration {
@Bean PersonEventHandler personEventHandler() {
return new PersonEventHandler();
}
@Bean AnnotatedHandlerRepositoryEventListener repositoryEventListener() {
return new AnnotatedHandlerRepositoryEventListener("com.mycompany.repository.handlers");
}
}
(In XML style):
<bean class="com.mycompany.repository.handlers.PersonEventHandler"/>
<bean class="org.springframework.data.rest.repository.context.AnnotatedHandlerRepositoryEventListener">
<property name="basePackage" value="com.mycompany.repository.handlers"/>
</bean>

View File

@@ -10,8 +10,8 @@ cglibVersion = 2.2
groovyVersion = 1.8.6
# Supporting libraries
sdCommonsVersion = 1.3.0.RC1
sdJpaVersion = 1.1.0.RC1
sdCommonsVersion = 1.3.0.RC2
sdJpaVersion = 1.1.0.BUILD-SNAPSHOT
jacksonVersion = 1.9.5
hibernateVersion = 4.1.1.Final

View File

@@ -1,7 +1,5 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -9,13 +7,9 @@ import java.util.Map;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.util.StringUtils;
@@ -26,9 +20,7 @@ import org.springframework.util.StringUtils;
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
R extends Repository<? extends Object, ? extends Serializable>,
E extends EntityMetadata<? extends AttributeMetadata>>
public abstract class RepositoryExporter<M extends RepositoryMetadata<E>, E extends EntityMetadata<? extends AttributeMetadata>>
implements ApplicationContextAware,
InitializingBean {
@@ -67,14 +59,12 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
@Override public void afterPropertiesSet() throws Exception {
repositories = new Repositories(applicationContext);
repositoryMetadata = new HashMap<String, M>();
Collection<RepositoryFactoryInformation> providers = BeanFactoryUtils.beansOfTypeIncludingAncestors(
applicationContext,
RepositoryFactoryInformation.class
).values();
for (RepositoryFactoryInformation entry : providers) {
EntityInformation entityInfo = entry.getEntityInformation();
Class<?> repoClass = entry.getRepositoryInterface();
for (Class<?> domainType : repositories) {
if (!exportOnlyTheseClasses.isEmpty() && !exportOnlyTheseClasses.contains(domainType.getName())) {
// Don't export this domain type
continue;
}
Class<?> repoClass = repositories.getRepositoryInformationFor(domainType).getRepositoryInterface();
String name;
RestResource pathSeg = repoClass.getAnnotation(RestResource.class);
if (null != pathSeg) {
@@ -82,9 +72,7 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
} else {
name = StringUtils.uncapitalize(repoClass.getSimpleName().replaceAll("Repository", ""));
}
R repo = (R) BeanFactoryUtils.beanOfTypeIncludingAncestors(applicationContext, repoClass);
M repoMeta = createRepositoryMetadata(repoClass, repo, name, entityInfo);
repositoryMetadata.put(name, repoMeta);
repositoryMetadata.put(name, createRepositoryMetadata(name, domainType, repoClass, repositories));
}
}
@@ -137,11 +125,9 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<R, E>,
return repositoryMetadata.get(name);
}
protected abstract M createRepositoryMetadata(
Class repoClass,
R repo,
String name,
EntityInformation entityInfo
);
protected abstract M createRepositoryMetadata(String name,
Class<?> domainType,
Class<?> repoClass,
Repositories repositories);
}

View File

@@ -3,6 +3,7 @@ package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.util.Map;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;
/**
@@ -10,7 +11,7 @@ import org.springframework.data.repository.Repository;
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public interface RepositoryMetadata<R extends Repository<? extends Object, ? extends Serializable>, E extends EntityMetadata<? extends AttributeMetadata>> {
public interface RepositoryMetadata<E extends EntityMetadata<? extends AttributeMetadata>> {
/**
* The name this {@link Repository} is exported under.
@@ -31,21 +32,21 @@ public interface RepositoryMetadata<R extends Repository<? extends Object, ? ext
*
* @return
*/
Class<? extends Object> domainType();
Class<?> domainType();
/**
* The Class of the {@link Repository} subinterface.
*
* @return
*/
Class<? extends Repository<? extends Object, ? extends Serializable>> repositoryClass();
Class<?> repositoryClass();
/**
* The {@link Repository} instance.
*
* @return
*/
R repository();
CrudRepository<Object, Serializable> repository();
/**
* The {@link EntityMetadata} associated with the domain type of this {@literal Repository}.

View File

@@ -1,6 +1,8 @@
package org.springframework.data.rest.repository.context;
/**
* Emitted after the entity is delete from the repository.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class AfterDeleteEvent extends RepositoryEvent {

View File

@@ -1,6 +1,8 @@
package org.springframework.data.rest.repository.context;
/**
* Emitted immediately after saving a linked object to its parent in the repository.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class AfterLinkSaveEvent extends LinkSaveEvent {

View File

@@ -1,8 +1,8 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* Emitted immediately after a save to the repository.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class AfterSaveEvent extends RepositoryEvent {

View File

@@ -41,6 +41,13 @@ public class AnnotatedHandlerRepositoryEventListener
private ApplicationContext applicationContext;
private Multimap<Class<? extends RepositoryEvent>, EventHandlerMethod> handlerMethods = ArrayListMultimap.create();
public AnnotatedHandlerRepositoryEventListener() {
}
public AnnotatedHandlerRepositoryEventListener(String basePackage) {
this.basePackage = basePackage;
}
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

View File

@@ -1,8 +1,8 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* Emitted before an entity is deleted from the repository.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class BeforeDeleteEvent extends RepositoryEvent {

View File

@@ -1,6 +1,8 @@
package org.springframework.data.rest.repository.context;
/**
* Emitted before a linked object is saved to the repository.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class BeforeLinkSaveEvent extends LinkSaveEvent {

View File

@@ -1,8 +1,8 @@
package org.springframework.data.rest.repository.context;
import org.springframework.context.ApplicationEvent;
/**
* Emitted before an entity is saved into the repository.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class BeforeSaveEvent extends RepositoryEvent {

View File

@@ -1,9 +1,11 @@
package org.springframework.data.rest.repository.context;
/**
* Base class for {@link RepositoryEvent}s that deal with saving a linked object.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class LinkSaveEvent extends RepositoryEvent {
public abstract class LinkSaveEvent extends RepositoryEvent {
private final Object linked;
@@ -12,6 +14,11 @@ public class LinkSaveEvent extends RepositoryEvent {
this.linked = linked;
}
/**
* Get the linked object.
*
* @return
*/
public Object getLinked() {
return linked;
}

View File

@@ -1,11 +1,10 @@
package org.springframework.data.rest.repository.jpa;
import java.io.Serializable;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.RepositoryExporter;
/**
@@ -13,10 +12,7 @@ import org.springframework.data.rest.repository.RepositoryExporter;
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class JpaRepositoryExporter extends RepositoryExporter<
JpaRepositoryMetadata<Repository<Object, Serializable>>,
Repository<Object, Serializable>,
JpaEntityMetadata> {
public class JpaRepositoryExporter extends RepositoryExporter<JpaRepositoryMetadata, JpaEntityMetadata> {
protected EntityManager entityManager;
@@ -27,17 +23,8 @@ public class JpaRepositoryExporter extends RepositoryExporter<
@SuppressWarnings({"unchecked"})
@Override
protected JpaRepositoryMetadata<Repository<Object, Serializable>> createRepositoryMetadata(
Class repoClass,
Repository<Object, Serializable> repo,
String name,
EntityInformation entityInfo) {
return new JpaRepositoryMetadata(repositories,
name,
repoClass,
repo,
entityInfo,
entityManager);
protected JpaRepositoryMetadata createRepositoryMetadata(String name, Class<?> domainType, Class<?> repoClass, Repositories repositories) {
return new JpaRepositoryMetadata(name, domainType, repoClass, repositories, entityManager);
}
}

View File

@@ -8,7 +8,7 @@ import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.metamodel.Metamodel;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.RepositoryMetadata;
@@ -20,11 +20,11 @@ import org.springframework.util.StringUtils;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> implements RepositoryMetadata<R, JpaEntityMetadata> {
public class JpaRepositoryMetadata implements RepositoryMetadata<JpaEntityMetadata> {
private final String name;
private final Class<? extends Repository<? extends Object, ? extends Serializable>> repoClass;
private final R repository;
private final Class<?> repoClass;
private final CrudRepository<Object, Serializable> repository;
private final EntityInformation entityInfo;
private final Map<String, RepositoryQueryMethod> queryMethods = new HashMap<String, RepositoryQueryMethod>();
@@ -32,16 +32,15 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
private JpaEntityMetadata entityMetadata;
@SuppressWarnings({"unchecked"})
public JpaRepositoryMetadata(Repositories repositories,
String name,
final Class<? extends Repository<? extends Object, ? extends Serializable>> repoClass,
R repository,
EntityInformation entityInfo,
public JpaRepositoryMetadata(String name,
Class<?> domainType,
final Class<?> repoClass,
Repositories repositories,
EntityManager entityManager) {
this.name = name;
this.repoClass = repoClass;
this.repository = repository;
this.entityInfo = entityInfo;
this.repository = repositories.getRepositoryFor(domainType);
this.entityInfo = repositories.getEntityInformationFor(domainType);
RestResource resourceAnno = repoClass.getAnnotation(RestResource.class);
if (null != resourceAnno && StringUtils.hasText(resourceAnno.rel())) {
@@ -50,26 +49,15 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
rel = name;
}
ReflectionUtils.doWithMethods(
repoClass,
new ReflectionUtils.MethodCallback() {
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
RestResource resourceAnno = method.getAnnotation(RestResource.class);
String pathSeg = resourceAnno.path();
ReflectionUtils.makeAccessible(method);
queryMethods.put(pathSeg, new RepositoryQueryMethod(method));
}
},
new ReflectionUtils.MethodFilter() {
@Override public boolean matches(Method method) {
return (!method.isSynthetic()
&& !method.isBridge()
&& method.getDeclaringClass() != Object.class
&& !method.getName().contains("$")
&& null != method.getAnnotation(RestResource.class));
}
}
);
for (Method method : repositories.getRepositoryInformationFor(domainType).getQueryMethods()) {
String pathSeg = method.getName();
RestResource methodResourceAnno = method.getAnnotation(RestResource.class);
if (null != methodResourceAnno) {
pathSeg = methodResourceAnno.path();
}
ReflectionUtils.makeAccessible(method);
queryMethods.put(pathSeg, new RepositoryQueryMethod(method));
}
Metamodel metamodel = entityManager.getMetamodel();
entityMetadata = new JpaEntityMetadata(repositories, metamodel.entity(entityInfo.getJavaType()));
@@ -83,15 +71,15 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
return rel;
}
@Override public Class<? extends Object> domainType() {
@Override public Class<?> domainType() {
return entityMetadata.type();
}
@Override public Class<? extends Repository<? extends Object, ? extends Serializable>> repositoryClass() {
@Override public Class<?> repositoryClass() {
return repoClass;
}
@Override public R repository() {
@Override public CrudRepository<Object, Serializable> repository() {
return repository;
}

View File

@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.data.rest.repository.spec.PersonEventHandler"/>
<bean class="org.springframework.data.rest.repository.context.AnnotatedHandlerRepositoryEventListener"
p:basePackage="org.springframework.data.rest.repository.spec"/>
<bean class="org.springframework.data.rest.repository.context.AnnotatedHandlerRepositoryEventListener">
<property name="basePackage" value="org.springframework.data.rest.repository.spec"/>
</bean>
</beans>

View File

@@ -58,7 +58,6 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.Assert;

View File

@@ -16,7 +16,6 @@ import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockServletConfig
import org.springframework.mock.web.MockServletContext
import org.springframework.orm.jpa.EntityManagerHolder
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionSynchronizationManager
import org.springframework.ui.ExtendedModelMap
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext
@@ -75,7 +74,6 @@ class RepositoryRestControllerSpec extends Specification {
}
}
@Transactional
def "API Test"() {
given:
@@ -99,7 +97,7 @@ class RepositoryRestControllerSpec extends Specification {
then:
model.status == HttpStatus.CREATED
when: "getting specific entity"
when: "getting a specific entity"
model.clear()
req = createRequest("GET", "people/1")
controller.entity(new ServletServerHttpRequest(req), uriBuilder, "people", "1", model)
@@ -126,7 +124,7 @@ class RepositoryRestControllerSpec extends Specification {
model.status == HttpStatus.OK
peopleLinks[0].href().toString() == "http://localhost:8080/data/people/1"
when: "creating child entity"
when: "creating a child entity"
model.clear()
req = createRequest("POST", "address")
data = mapper.writeValueAsBytes(new Address(["1 W. 1st St."] as String[], "Univille", "ST", "12345"))
@@ -147,7 +145,7 @@ class RepositoryRestControllerSpec extends Specification {
then:
model.status == HttpStatus.CREATED
when: "getting property of entity"
when: "getting property of an entity"
model.clear()
controller.propertyOfEntity(uriBuilder, "people", "1", "addresses", model)
def addrLinks = model.resource?.links