From 88fc8ba7cd1d8c43c9acbedab3f6cc4fafe769e2 Mon Sep 17 00:00:00 2001 From: Jon Brisbin Date: Tue, 7 Aug 2012 10:02:16 -0500 Subject: [PATCH] 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. --- build.gradle | 2 +- gradle.properties | 4 +- .../HandleBeforeRenderResource.java | 19 ++++ .../HandleBeforeRenderResources.java | 19 ++++ .../AbstractRepositoryEventListener.java | 41 ++++++++- ...notatedHandlerRepositoryEventListener.java | 62 +++++++++---- .../context/BeforeRenderResourceEvent.java | 13 +++ .../context/BeforeRenderResourcesEvent.java | 17 ++++ .../rest/repository/context/RenderEvent.java | 54 ++++++++++++ .../repository/spec/ExtensionsSpec.groovy | 87 +++++++++++++++++-- .../repository/test/ApplicationConfig.java | 2 +- .../test/resources/ExtensionsSpec-test.xml | 1 + .../rest/webmvc/RepositoryRestController.java | 51 +++++------ .../data/rest/webmvc/spec/EventsSpec.groovy | 4 + 14 files changed, 316 insertions(+), 60 deletions(-) create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResource.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResources.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeRenderResourceEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeRenderResourcesEvent.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RenderEvent.java diff --git a/build.gradle b/build.gradle index fe292cdb6..e80719423 100644 --- a/build.gradle +++ b/build.gradle @@ -88,7 +88,7 @@ configure(subprojects) { subproject -> } -task wrapper(type: Wrapper) { gradleVersion = "1.0" } +task wrapper(type: Wrapper) { gradleVersion = "1.1" } idea { module { diff --git a/gradle.properties b/gradle.properties index 16a40318d..e35f31c16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResource.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResource.java new file mode 100644 index 000000000..c1db22198 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResource.java @@ -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 {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResources.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResources.java new file mode 100644 index 000000000..1348d9ab3 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/HandleBeforeRenderResources.java @@ -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 {}; + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java index 96e9b1752..6b9875c3a 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java @@ -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 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 params = new ArrayList(); - 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 params = new ArrayList(); + 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); } } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeRenderResourceEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeRenderResourceEvent.java new file mode 100644 index 000000000..cebe6c3d6 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeRenderResourceEvent.java @@ -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); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeRenderResourcesEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeRenderResourcesEvent.java new file mode 100644 index 000000000..ae32c531d --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeRenderResourcesEvent.java @@ -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); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RenderEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RenderEvent.java new file mode 100644 index 000000000..dd2d97175 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RenderEvent.java @@ -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; + } + +} diff --git a/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy b/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy index f8e0465a7..d42718f8d 100644 --- a/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy +++ b/spring-data-rest-repository/src/test/groovy/org/springframework/data/rest/repository/spec/ExtensionsSpec.groovy @@ -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 */ -@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")) + } + +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java index 379192579..ceac6c0fd 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java @@ -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 { diff --git a/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml b/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml index 89016685e..0bfa9eb43 100644 --- a/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml +++ b/spring-data-rest-repository/src/test/resources/ExtensionsSpec-test.xml @@ -4,6 +4,7 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java index 9c01b822b..76e90b784 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java @@ -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 extractPropertiesLinkAware(String repoRel, - Object entity, - EntityMetadata entityMetadata, - URI baseUri) { - final Map entityDto = new HashMap(); - - for(Map.Entry 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 links = (List)entityDto.get(LINKS); - if(null == links) { - links = new ArrayList(); - entityDto.put(LINKS, links); - } - links.add(l); - } - - return entityDto; - } - */ - private boolean shouldReturnLinks(String acceptHeader) { if(null != acceptHeader) { List accept = MediaType.parseMediaTypes(acceptHeader); diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy index c08ded450..4ad694b1e 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy @@ -44,4 +44,8 @@ class EventsSpec extends BaseSpec { } + def "captures resource rendering events"() { + + } + }