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"() { + + } + }