Added event handling machinery to intercept render events. It's now possible to write and event handler to influence the resource objects that are being sent to the client. There is an abstract helper class and there is support for annotated POJOs.

This commit is contained in:
Jon Brisbin
2012-08-07 10:02:16 -05:00
parent f3dde0c144
commit 88fc8ba7cd
14 changed files with 316 additions and 60 deletions

View File

@@ -88,7 +88,7 @@ configure(subprojects) { subproject ->
}
task wrapper(type: Wrapper) { gradleVersion = "1.0" }
task wrapper(type: Wrapper) { gradleVersion = "1.1" }
idea {
module {

View File

@@ -1,10 +1,10 @@
# Logging
slf4jVersion = 1.6.6
logbackVersion = 1.0.3
logbackVersion = 1.0.6
# Libraries
springVersion = 3.1.2.RELEASE
cglibVersion = 2.2
cglibVersion = 2.2.2
# Languages
groovyVersion = 1.8.6

View File

@@ -0,0 +1,19 @@
package org.springframework.data.rest.repository.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeRenderResource {
Class<?>[] value() default {};
}

View File

@@ -0,0 +1,19 @@
package org.springframework.data.rest.repository.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jon Brisbin
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface HandleBeforeRenderResources {
Class<?>[] value() default {};
}

View File

@@ -7,8 +7,12 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.data.rest.core.Resource;
import org.springframework.data.rest.core.Resources;
import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.http.server.ServerHttpRequest;
/**
* Abstract class that listens for generic {@link RepositoryEvent}s and dispatches them to a specific
@@ -45,11 +49,18 @@ public abstract class AbstractRepositoryEventListener<T extends AbstractReposito
} else if(event instanceof BeforeLinkDeleteEvent) {
onBeforeLinkDelete(event.getSource(), ((BeforeLinkDeleteEvent)event).getLinked());
} else if(event instanceof AfterLinkDeleteEvent) {
onAfterLinkDelete(event.getSource(), ((BeforeLinkDeleteEvent)event).getLinked());
onAfterLinkDelete(event.getSource(), ((AfterLinkDeleteEvent)event).getLinked());
} else if(event instanceof BeforeDeleteEvent) {
onBeforeDelete(event.getSource());
} else if(event instanceof AfterDeleteEvent) {
onAfterDelete(event.getSource());
} else if(event instanceof RenderEvent) {
RenderEvent ev = (RenderEvent)event;
if(ev.isTopLevelResource()) {
onBeforeRenderResources(ev.getRequest(), ev.getRepositoryMetadata(), ev.getResources());
} else {
onBeforeRenderResource(ev.getRequest(), ev.getRepositoryMetadata(), ev.getResource());
}
}
}
@@ -121,4 +132,32 @@ public abstract class AbstractRepositoryEventListener<T extends AbstractReposito
protected void onAfterDelete(Object entity) {
}
/**
* Override this method if you are interested in {@literal beforeRender} for top-level events. These are events
* triggered by the exporter before sending out a wrapped, top-level response for queries, entity lists, and results
* that are pagable.
*
* @param request
* @param repositoryMetadata
* @param resources
*/
protected void onBeforeRenderResources(ServerHttpRequest request,
RepositoryMetadata repositoryMetadata,
Resources resources) {
}
/**
* Override this method if you are interested in {@literal beforeRender} for entity events. These are events emitted
* by the exporter before sending out an entity representation to the client. These events are triggered when
* requesting individual entities and specific properties of an entity.
*
* @param request
* @param repositoryMetadata
* @param resource
*/
protected void onBeforeRenderResource(ServerHttpRequest request,
RepositoryMetadata repositoryMetadata,
Resource resource) {
}
}

View File

@@ -21,6 +21,8 @@ import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave;
import org.springframework.data.rest.repository.annotation.HandleAfterSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeRenderResource;
import org.springframework.data.rest.repository.annotation.HandleBeforeRenderResources;
import org.springframework.data.rest.repository.annotation.HandleBeforeSave;
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler;
import org.springframework.util.ClassUtils;
@@ -96,8 +98,7 @@ public class AnnotatedHandlerRepositoryEventListener implements ApplicationListe
return this;
}
@Override public void afterPropertiesSet()
throws Exception {
@Override public void afterPropertiesSet() throws Exception {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(RepositoryEventHandler.class, true, true));
for(BeanDefinition beanDef : scanner.findCandidateComponents(basePackage)) {
@@ -121,6 +122,16 @@ public class AnnotatedHandlerRepositoryEventListener implements ApplicationListe
inspect(targetType, handler, method, HandleAfterLinkSave.class, AfterLinkSaveEvent.class);
inspect(targetType, handler, method, HandleBeforeDelete.class, BeforeDeleteEvent.class);
inspect(targetType, handler, method, HandleAfterDelete.class, AfterDeleteEvent.class);
inspect(targetType,
handler,
method,
HandleBeforeRenderResource.class,
BeforeRenderResourceEvent.class);
inspect(targetType,
handler,
method,
HandleBeforeRenderResources.class,
BeforeRenderResourcesEvent.class);
}
},
new ReflectionUtils.MethodFilter() {
@@ -139,23 +150,40 @@ public class AnnotatedHandlerRepositoryEventListener implements ApplicationListe
@Override public void onApplicationEvent(RepositoryEvent event) {
Class<? extends RepositoryEvent> eventType = event.getClass();
if(handlerMethods.containsKey(eventType)) {
for(EventHandlerMethod handlerMethod : handlerMethods.get(eventType)) {
try {
Object src = event.getSource();
if(ClassUtils.isAssignable(handlerMethod.targetType, src.getClass())) {
List<Object> params = new ArrayList<Object>();
params.add(src);
if(event instanceof BeforeLinkSaveEvent) {
params.add(((BeforeLinkSaveEvent)event).getLinked());
} else if(event instanceof AfterLinkSaveEvent) {
params.add(((AfterLinkSaveEvent)event).getLinked());
}
handlerMethod.method.invoke(handlerMethod.handler, params.toArray());
if(!handlerMethods.containsKey(eventType)) {
return;
}
for(EventHandlerMethod handlerMethod : handlerMethods.get(eventType)) {
try {
Object src = event.getSource();
if(event instanceof RenderEvent) {
RenderEvent ev = (RenderEvent)event;
if(!ClassUtils.isAssignable(handlerMethod.targetType,
ev.getRepositoryMetadata().entityMetadata().type())) {
continue;
}
} catch(Exception e) {
throw new IllegalStateException(e);
} else if(!ClassUtils.isAssignable(handlerMethod.targetType, src.getClass())) {
continue;
}
List<Object> params = new ArrayList<Object>();
params.add(src);
if(event instanceof BeforeLinkSaveEvent) {
params.add(((BeforeLinkSaveEvent)event).getLinked());
} else if(event instanceof AfterLinkSaveEvent) {
params.add(((AfterLinkSaveEvent)event).getLinked());
} else if(event instanceof RenderEvent) {
RenderEvent ev = (RenderEvent)event;
params.add(0, ev.getRequest());
params.add(1, ev.getRepositoryMetadata());
}
handlerMethod.method.invoke(handlerMethod.handler, params.toArray());
} catch(Exception e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -0,0 +1,13 @@
package org.springframework.data.rest.repository.context;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.http.server.ServerHttpRequest;
/**
* @author Jon Brisbin
*/
public class BeforeRenderResourceEvent extends RenderEvent {
public BeforeRenderResourceEvent(ServerHttpRequest request, RepositoryMetadata repoMeta, Object source) {
super(request, repoMeta, source);
}
}

View File

@@ -0,0 +1,17 @@
package org.springframework.data.rest.repository.context;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.http.server.ServerHttpRequest;
/**
* Event emitted before the the object is rendered to the client. Implementations of {@link
* AbstractRepositoryEventListener} can listen for these events and alter the output of the resource being sent to the
* link.
*
* @author Jon Brisbin
*/
public class BeforeRenderResourcesEvent extends RenderEvent {
public BeforeRenderResourcesEvent(ServerHttpRequest request, RepositoryMetadata repoMeta, Object source) {
super(request, repoMeta, source);
}
}

View File

@@ -0,0 +1,54 @@
package org.springframework.data.rest.repository.context;
import org.springframework.data.rest.core.Resource;
import org.springframework.data.rest.core.Resources;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* @author Jon Brisbin
*/
public abstract class RenderEvent extends RepositoryEvent {
protected final ServerHttpRequest request;
protected final RepositoryMetadata repositoryMetadata;
protected final boolean topLevelResource;
public RenderEvent(ServerHttpRequest request, RepositoryMetadata repoMeta, Object source) {
super(source);
Assert.isTrue(source instanceof Resource || source instanceof Resources);
this.request = request;
this.repositoryMetadata = repoMeta;
this.topLevelResource = (source instanceof Resources);
}
public ServerHttpRequest getRequest() {
return request;
}
public RepositoryMetadata getRepositoryMetadata() {
return repositoryMetadata;
}
public Resource getResource() {
if(getSource() instanceof Resource) {
return (Resource)getSource();
} else {
throw new IllegalStateException("Source of event is not a Resource, it's " + source);
}
}
public Resources getResources() {
if(getSource() instanceof Resources) {
return (Resources)getSource();
} else {
throw new IllegalStateException("Source of event is not a Resources, it's " + source);
}
}
public boolean isTopLevelResource() {
return topLevelResource;
}
}

View File

@@ -2,35 +2,49 @@ package org.springframework.data.rest.repository.spec
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.rest.core.Resource
import org.springframework.data.rest.core.Resources
import org.springframework.data.rest.core.SimpleLink
import org.springframework.data.rest.repository.RepositoryExporter
import org.springframework.data.rest.repository.RepositoryMetadata
import org.springframework.data.rest.repository.annotation.HandleAfterDelete
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave
import org.springframework.data.rest.repository.annotation.HandleAfterSave
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave
import org.springframework.data.rest.repository.annotation.HandleBeforeRenderResource
import org.springframework.data.rest.repository.annotation.HandleBeforeRenderResources
import org.springframework.data.rest.repository.annotation.HandleBeforeSave
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler
import org.springframework.data.rest.repository.context.AfterLinkSaveEvent
import org.springframework.data.rest.repository.context.AfterDeleteEvent
import org.springframework.data.rest.repository.context.AfterLinkSaveEvent
import org.springframework.data.rest.repository.context.AfterSaveEvent
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent
import org.springframework.data.rest.repository.context.AnnotatedHandlerRepositoryEventListener
import org.springframework.data.rest.repository.context.BeforeDeleteEvent
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent
import org.springframework.data.rest.repository.context.BeforeRenderResourceEvent
import org.springframework.data.rest.repository.context.BeforeRenderResourcesEvent
import org.springframework.data.rest.repository.context.BeforeSaveEvent
import org.springframework.data.rest.repository.test.ApplicationConfig
import org.springframework.data.rest.repository.test.Person
import org.springframework.http.server.ServerHttpRequest
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@ContextConfiguration(locations = ["/ExtensionsSpec-test.xml"])
@ContextConfiguration(classes = [ApplicationConfig, EventsApplicationConfig])
class ExtensionsSpec extends Specification {
@Autowired
ApplicationContext appCtx
@Autowired
PersonEventHandler handler
@Autowired
RepositoryExporter exporter
def "responds to ApplicationEvents in annotated handlers"() {
@@ -55,6 +69,48 @@ class ExtensionsSpec extends Specification {
}
def "responds to render events"() {
given:
def repoMeta = exporter.repositoryMetadataFor(Person)
def request = Mock(ServerHttpRequest)
def p = new Person("John Doe")
def selfLink = new SimpleLink("self", new URI("http://localhost:8080/people/1"))
def resources = new Resources()
resources.links << selfLink
def resource = new Resource(p)
resource.links << selfLink
when:
appCtx.publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources))
appCtx.publishEvent(new BeforeRenderResourceEvent(request, repoMeta, resource))
then:
resources.links.size() == 2
null != resources.links.find { it.rel() == "linkAddedByHandler" }
resource.links.size() == 2
null != resources.links.find { it.rel() == "linkAddedByHandler" }
}
}
@Configuration
class EventsApplicationConfig {
@Bean AnnotatedHandlerRepositoryEventListener repositoryEventListener() {
new AnnotatedHandlerRepositoryEventListener("org.springframework.data.rest.repository.spec");
}
@Bean PersonEventHandler personEventHandler() {
new PersonEventHandler()
}
@Bean PersonRenderHandler personRenderHandler() {
new PersonRenderHandler()
}
}
@RepositoryEventHandler(Person)
@@ -92,3 +148,20 @@ class PersonEventHandler {
}
}
@RepositoryEventHandler(Person)
class PersonRenderHandler {
@HandleBeforeRenderResources void handleBeforeRenderResources(ServerHttpRequest request,
RepositoryMetadata repoMeta,
Resources resources) {
resources.links << new SimpleLink("linkAddedByHandler", new URI("http://localhost:8080/linkAddedByHandler"))
}
@HandleBeforeRenderResource void handleBeforeRenderResource(ServerHttpRequest request,
RepositoryMetadata repoMeta,
Resource resource) {
resource.links << new SimpleLink("linkAddedByHandler", new URI("http://localhost:8080/linkAddedByHandler"))
}
}

View File

@@ -23,7 +23,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
* @author Jon Brisbin
*/
@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
@ComponentScan(basePackageClasses = {ApplicationConfig.class})
@EnableJpaRepositories
@EnableTransactionManagement
public class ApplicationConfig {

View File

@@ -4,6 +4,7 @@
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.spec.PersonRenderHandler"/>
<bean class="org.springframework.data.rest.repository.context.AnnotatedHandlerRepositoryEventListener">
<property name="basePackage" value="org.springframework.data.rest.repository.spec"/>

View File

@@ -65,6 +65,8 @@ import org.springframework.data.rest.repository.context.AfterSaveEvent;
import org.springframework.data.rest.repository.context.BeforeDeleteEvent;
import org.springframework.data.rest.repository.context.BeforeLinkDeleteEvent;
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent;
import org.springframework.data.rest.repository.context.BeforeRenderResourceEvent;
import org.springframework.data.rest.repository.context.BeforeRenderResourcesEvent;
import org.springframework.data.rest.repository.context.BeforeSaveEvent;
import org.springframework.data.rest.repository.context.RepositoryEvent;
import org.springframework.data.rest.repository.invoke.CrudMethod;
@@ -326,6 +328,8 @@ public class RepositoryRestController
}
}
publishEvent(new BeforeRenderResourcesEvent(request, null, resources));
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
@@ -438,6 +442,8 @@ public class RepositoryRestController
buildUri(baseUri, repository, "search")));
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
@@ -490,6 +496,8 @@ public class RepositoryRestController
}
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
@@ -664,6 +672,8 @@ public class RepositoryRestController
}
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
@@ -726,6 +736,8 @@ public class RepositoryRestController
body = resource;
}
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, body));
return negotiateResponse(request, HttpStatus.CREATED, headers, body);
}
@@ -787,6 +799,8 @@ public class RepositoryRestController
URI selfUri = buildUri(baseUri, repository, id);
res.addLink(new SimpleLink(SELF, selfUri));
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, res));
return negotiateResponse(request, HttpStatus.OK, headers, res);
}
@@ -871,6 +885,8 @@ public class RepositoryRestController
baseUri);
res.addLink(new SimpleLink(SELF, selfUri));
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, body));
body = res;
}
@@ -1025,6 +1041,8 @@ public class RepositoryRestController
res.addLink(new SimpleLink(rel, path));
}
publishEvent(new BeforeRenderResourceEvent(request, propRepoMeta, res));
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), res);
}
@@ -1274,6 +1292,8 @@ public class RepositoryRestController
URI selfUri = buildUri(baseUri, linkedRepoMeta.name(), linkedId);
res.addLink(new SimpleLink(SELF, selfUri));
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, res));
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Location", selfUri.toString());
@@ -1629,37 +1649,6 @@ public class RepositoryRestController
return resource;
}
/*
@SuppressWarnings({"unchecked"})
private Map<String, Object> extractPropertiesLinkAware(String repoRel,
Object entity,
EntityMetadata<AttributeMetadata> entityMetadata,
URI baseUri) {
final Map<String, Object> entityDto = new HashMap<String, Object>();
for(Map.Entry<String, AttributeMetadata> attrMeta : entityMetadata.embeddedAttributes().entrySet()) {
String name = attrMeta.getKey();
Object val = attrMeta.getValue().get(entity);
if(null != val) {
entityDto.put(name, val);
}
}
for(String attrName : entityMetadata.linkedAttributes().keySet()) {
URI uri = buildUri(baseUri, attrName);
Link l = new SimpleLink(repoRel + "." + entity.getClass().getSimpleName() + "." + attrName, uri);
List<Link> links = (List<Link>)entityDto.get(LINKS);
if(null == links) {
links = new ArrayList<Link>();
entityDto.put(LINKS, links);
}
links.add(l);
}
return entityDto;
}
*/
private boolean shouldReturnLinks(String acceptHeader) {
if(null != acceptHeader) {
List<MediaType> accept = MediaType.parseMediaTypes(acceptHeader);

View File

@@ -44,4 +44,8 @@ class EventsSpec extends BaseSpec {
}
def "captures resource rendering events"() {
}
}