Added configuration property for domain type to repository mappings. Also improved the post-processing support, though I think there's still some room for tweaking.

This commit is contained in:
Jon Brisbin
2012-08-20 15:06:15 -05:00
committed by Jon Brisbin
parent 2000d07b32
commit 3aa8905b15
10 changed files with 210 additions and 68 deletions

View File

@@ -1,5 +1,7 @@
package org.springframework.data.rest.core;
import org.springframework.http.server.ServerHttpRequest;
/**
* Implementations of this interface will post-process objects to mutate them in ways meaningful to the context in
* which they are called.
@@ -13,6 +15,6 @@ public interface PostProcessor<T> {
*
* @param obj
*/
T postProcess(T obj);
T postProcess(ServerHttpRequest request, T obj);
}

View File

@@ -20,7 +20,7 @@ import org.springframework.util.StringUtils;
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public abstract class RepositoryExporter<M extends RepositoryMetadata<E>, E extends EntityMetadata<? extends AttributeMetadata>>
public abstract class RepositoryExporter<R extends RepositoryExporter<? super R, M, E>, M extends RepositoryMetadata<E>, E extends EntityMetadata<? extends AttributeMetadata>>
implements ApplicationContextAware,
InitializingBean {
@@ -49,9 +49,9 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<E>, E exte
* @return @this
*/
@SuppressWarnings({"unchecked"})
public M setExportOnlyTheseClasses(List<String> exportOnlyTheseClasses) {
public R setExportOnlyTheseClasses(List<String> exportOnlyTheseClasses) {
this.exportOnlyTheseClasses = exportOnlyTheseClasses;
return (M)this;
return (R)this;
}
public Map<Class<?>, Class<?>> getDomainTypeMappings() {
@@ -59,13 +59,12 @@ public abstract class RepositoryExporter<M extends RepositoryMetadata<E>, E exte
}
@SuppressWarnings({"unchecked"})
public M setDomainTypeMappings(Map<Class<?>, Class<?>> domainTypeMappings) {
public R setDomainTypeMappings(Map<Class<?>, Class<?>> domainTypeMappings) {
this.domainTypeMappings = domainTypeMappings;
return (M)this;
return (R)this;
}
@Override public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

View File

@@ -13,7 +13,7 @@ import org.springframework.data.rest.repository.RepositoryExporter;
* @author Jon Brisbin
*/
public class JpaRepositoryExporter
extends RepositoryExporter<JpaRepositoryMetadata, JpaEntityMetadata> {
extends RepositoryExporter<JpaRepositoryExporter, JpaRepositoryMetadata, JpaEntityMetadata> {
protected EntityManager entityManager;

View File

@@ -3,9 +3,11 @@ package org.springframework.data.rest.webmvc;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
@@ -20,17 +22,18 @@ public class RepositoryRestConfiguration {
public static final RepositoryRestConfiguration DEFAULT = new RepositoryRestConfiguration();
private int defaultPageSize = 20;
private String pageParamName = "page";
private String limitParamName = "limit";
private String sortParamName = "sort";
private String jsonpParamName = "callback";
private String jsonpOnErrParamName = null;
private List<HttpMessageConverter<?>> customConverters = Collections.emptyList();
private Multimap<Class<?>, ResourcePostProcessor> resourcePostProcessors = ArrayListMultimap.create();
private List<ResponsePostProcessor> responsePostProcessors = Collections.emptyList();
private MediaType defaultMediaType = MediaType.APPLICATION_JSON;
private boolean dumpErrors = true;
private int defaultPageSize = 20;
private String pageParamName = "page";
private String limitParamName = "limit";
private String sortParamName = "sort";
private String jsonpParamName = "callback";
private String jsonpOnErrParamName = null;
private List<HttpMessageConverter<?>> customConverters = Collections.emptyList();
private Multimap<Class<?>, ResourcePostProcessor> resourcePostProcessors = ArrayListMultimap.create();
private List<ResourceSetPostProcessor> resourceSetPostProcessors = Collections.emptyList();
private Map<Class<?>, Class<?>> typeMappings = Collections.emptyMap();
private MediaType defaultMediaType = MediaType.APPLICATION_JSON;
private boolean dumpErrors = true;
/**
* Get the default size of {@link org.springframework.data.domain.Pageable}s. Default is 20.
@@ -143,6 +146,29 @@ public class RepositoryRestConfiguration {
return this;
}
/**
* Get the list of domain type to repository implementation mappings that will help the exporters narrow down the
* correct {@link org.springframework.data.repository.Repository} to return for a given domain type.
*
* @return
*/
public Map<Class<?>, Class<?>> getDomainTypeToRepositoryMappings() {
return typeMappings;
}
/**
* Set the list of domain type to repository implementation mappings that will help the exporters narrow down the
* correct {@link org.springframework.data.repository.Repository} to return for a given domain type.
*
* @param typeMappings
*
* @return
*/
public RepositoryRestConfiguration setDomainTypeToRepositoryMappings(Map<Class<?>, Class<?>> typeMappings) {
this.typeMappings = typeMappings;
return this;
}
/**
* Get the name of the URL query string parameter that indicates the name of the javascript function to use as the
* JSONP wrapper for results.
@@ -232,22 +258,25 @@ public class RepositoryRestConfiguration {
}
/**
* Get the list of {@link ResponsePostProcessor}s that will potentially alter the responses going back to the
* Get the list of {@link ResourceSetPostProcessor}s that will potentially alter the responses going back to the
* client.
*
* @return
*/
public List<ResponsePostProcessor> getResponsePostProcessors() {
return responsePostProcessors;
public List<ResourceSetPostProcessor> getResourceSetPostProcessors() {
return resourceSetPostProcessors;
}
/**
* Set the list of {@link ResponsePostProcessor}s that will potentially alter the responses going back to the
* Set the list of {@link ResourceSetPostProcessor}s that will potentially alter the responses going back to the
*
* @param responsePostProcessors
* @param resourceSetPostProcessors
*/
public void setResponsePostProcessors(List<ResponsePostProcessor> responsePostProcessors) {
this.responsePostProcessors = responsePostProcessors;
@Autowired(required = false)
public RepositoryRestConfiguration setResourceSetPostProcessors(List<ResourceSetPostProcessor> resourceSetPostProcessors) {
Assert.notNull(resourceSetPostProcessors, "ResourceSetPostProcessors cannot be null.");
this.resourceSetPostProcessors = resourceSetPostProcessors;
return this;
}
/**
@@ -259,10 +288,29 @@ public class RepositoryRestConfiguration {
* @return
*/
public RepositoryRestConfiguration addResourcePostProcessor(Class<?> type, ResourcePostProcessor postProcessor) {
Assert.notNull(type, "Type for ResourcePostProcessor cannot be null.");
Assert.notNull(postProcessor, "ResourcePostProcessor for type " + type.getName() + " cannot be null.");
resourcePostProcessors.put(type, postProcessor);
return this;
}
/**
* Set tje {@link ResourcePostProcessor} map used to determine what post-processors to run for which domain type.
*
* @param postProcessors
*
* @return
*/
public RepositoryRestConfiguration setResourcePostProcessors(Map<Class<?>, ResourcePostProcessor> postProcessors) {
if(null == postProcessors) {
return this;
}
for(Map.Entry<Class<?>, ResourcePostProcessor> entry : postProcessors.entrySet()) {
addResourcePostProcessor(entry.getKey(), entry.getValue());
}
return this;
}
/**
* Get the {@link ResourcePostProcessor}s assigned to a particular domain type.
*

View File

@@ -363,7 +363,12 @@ public class RepositoryRestController
}
}
// Publish an event that we're about to publish this ResourceSet
publishEvent(new BeforeRenderResourcesEvent(request, null, resources));
// Run any configured post processors
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
@@ -401,7 +406,7 @@ public class RepositoryRestController
}
Iterator allEntities = Collections.emptyList().iterator();
final ResourceSet resources;
ResourceSet resources;
if(repoMeta.repository() instanceof PagingAndSortingRepository) {
PageableResourceSet pr = new PageableResourceSet();
@@ -480,6 +485,10 @@ public class RepositoryRestController
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
// Run any configured post processors
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
@@ -535,6 +544,10 @@ public class RepositoryRestController
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
// Run any configured post processors
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
@@ -715,6 +728,10 @@ public class RepositoryRestController
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
// Run any configured post processors
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
@@ -780,6 +797,10 @@ public class RepositoryRestController
body = resource;
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, body));
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(repoMeta.domainType())) {
body = pp.postProcess(request, body);
}
}
return negotiateResponse(request, HttpStatus.CREATED, headers, body);
@@ -839,13 +860,17 @@ public class RepositoryRestController
}
URI selfUri = buildUri(baseUri, repository, id);
MapResource res = createResource(repoMeta.rel(),
entity,
repoMeta.entityMetadata(),
selfUri);
Resource res = createResource(repoMeta.rel(),
entity,
repoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(SELF, selfUri));
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, res));
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(repoMeta.domainType())) {
res = pp.postProcess(request, res);
}
return negotiateResponse(request, HttpStatus.OK, headers, res);
}
@@ -926,15 +951,19 @@ public class RepositoryRestController
Object body = null;
if(returnBody(request)) {
MapResource res = createResource(repoMeta.rel(),
savedEntity,
repoMeta.entityMetadata(),
selfUri);
Resource res = createResource(repoMeta.rel(),
savedEntity,
repoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(SELF, selfUri));
body = res;
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, body));
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(repoMeta.domainType())) {
res = pp.postProcess(request, res);
}
}
if(!isUpdate) {
@@ -1084,6 +1113,10 @@ public class RepositoryRestController
}
}
body = resources;
// Run any post-processors for this domain type
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
} else if(propVal instanceof Map) {
propertyRel += "." + propRepoMeta.entityMetadata().type().getSimpleName();
Map resource = new HashMap();
@@ -1098,13 +1131,17 @@ public class RepositoryRestController
resource.put(sKey, new ResourceLink(propertyRel, path));
} else {
URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId);
MapResource res = createResource(propRepoMeta.rel(),
entry.getValue(),
propRepoMeta.entityMetadata(),
selfUri);
Resource res = createResource(propRepoMeta.rel(),
entry.getValue(),
propRepoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(SELF, selfUri));
res.addLink(new ResourceLink(propertyRel, path));
resource.put(sKey, res);
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(propRepoMeta.domainType())) {
res = pp.postProcess(request, res);
}
}
}
body = new MapResource(resource);
@@ -1125,6 +1162,10 @@ public class RepositoryRestController
res.addLink(new ResourceLink(SELF, selfUri));
body = res;
}
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(propRepoMeta.domainType())) {
body = pp.postProcess(request, (Resource)body);
}
}
publishEvent(new BeforeRenderResourceEvent(request, propRepoMeta, body));
@@ -1388,14 +1429,18 @@ public class RepositoryRestController
String propertyRel = repository + "." + repoMeta.entityMetadata().type().getSimpleName() + "." + property;
URI propertyPath = buildUri(baseUri, repository, id, property, linkedId);
URI selfUri = buildUri(baseUri, linkedRepoMeta.name(), linkedId);
MapResource res = createResource(linkedRepoMeta.rel(),
linkedEntity,
linkedRepoMeta.entityMetadata(),
selfUri);
Resource res = createResource(linkedRepoMeta.rel(),
linkedEntity,
linkedRepoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(propertyRel, propertyPath));
res.addLink(new ResourceLink(SELF, selfUri));
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, res));
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(linkedRepoMeta.domainType())) {
res = pp.postProcess(request, res);
}
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Location", selfUri.toString());

View File

@@ -31,20 +31,20 @@ public class RepositoryRestMvcConfiguration {
* {@link org.springframework.data.rest.repository.RepositoryExporter} implementation for exporting JPA repositories.
*/
@Autowired(required = false)
JpaRepositoryExporter customJpaRepositoryExporter;
protected JpaRepositoryExporter customJpaRepositoryExporter;
/**
* {@link org.springframework.context.ApplicationListener} implementation for invoking {@link
* org.springframework.validation.Validator} instances assigned to specific domain types.
*/
@Autowired(required = false)
ValidatingRepositoryEventListener validatingRepositoryEventListener;
protected ValidatingRepositoryEventListener validatingRepositoryEventListener;
/**
* Main configuration for the REST exporter.
*/
@Autowired(required = false)
RepositoryRestConfiguration repositoryRestConfig = RepositoryRestConfiguration.DEFAULT;
protected RepositoryRestConfiguration repositoryRestConfig;
/**
* For getting access to the {@link javax.persistence.EntityManagerFactory}.
@@ -61,11 +61,9 @@ public class RepositoryRestMvcConfiguration {
* @return
*/
@Bean public JpaRepositoryExporter jpaRepositoryExporter() {
if(null == customJpaRepositoryExporter) {
return new JpaRepositoryExporter();
} else {
return customJpaRepositoryExporter;
}
return (null == customJpaRepositoryExporter
? new JpaRepositoryExporter().setDomainTypeMappings(repositoryRestConfiguration().getDomainTypeToRepositoryMappings())
: customJpaRepositoryExporter);
}
/**
@@ -74,11 +72,9 @@ public class RepositoryRestMvcConfiguration {
* @return
*/
@Bean public ValidatingRepositoryEventListener validatingRepositoryEventListener() {
if(null == validatingRepositoryEventListener) {
return new ValidatingRepositoryEventListener();
} else {
return validatingRepositoryEventListener;
}
return (null == validatingRepositoryEventListener
? new ValidatingRepositoryEventListener()
: validatingRepositoryEventListener);
}
/**
@@ -92,7 +88,8 @@ public class RepositoryRestMvcConfiguration {
}
/**
* A {@link org.springframework.data.rest.core.Resolver} implementation that takes a {@link java.net.URI} and turns it
* A {@link org.springframework.data.rest.core.Resolver} implementation that takes a {@link java.net.URI} and turns
* it
* into a top-level domain object.
*
* @return
@@ -101,6 +98,12 @@ public class RepositoryRestMvcConfiguration {
return new UriToDomainObjectResolver();
}
@Bean public RepositoryRestConfiguration repositoryRestConfiguration() {
return (null == repositoryRestConfig
? RepositoryRestConfiguration.DEFAULT
: repositoryRestConfig);
}
/**
* The main REST exporter Spring MVC controller.
*
@@ -113,7 +116,7 @@ public class RepositoryRestMvcConfiguration {
}
@Bean ResourcesReturnValueHandler resourcesReturnValueHandler() {
return new ResourcesReturnValueHandler(repositoryRestConfig);
return new ResourcesReturnValueHandler(repositoryRestConfiguration());
}
/**
@@ -123,7 +126,7 @@ public class RepositoryRestMvcConfiguration {
* @return
*/
@Bean public RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() {
return new RepositoryRestHandlerAdapter(repositoryRestConfig);
return new RepositoryRestHandlerAdapter(repositoryRestConfiguration());
}
/**

View File

@@ -9,5 +9,5 @@ import org.springframework.data.rest.core.ResourceSet;
*
* @author Jon Brisbin
*/
public interface ResponsePostProcessor extends PostProcessor<ResourceSet> {
public interface ResourceSetPostProcessor extends PostProcessor<ResourceSet> {
}

View File

@@ -1,7 +1,9 @@
package org.springframework.data.rest.test.webmvc;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
@@ -12,9 +14,11 @@ import org.springframework.context.annotation.Import;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.rest.webmvc.RepositoryRestConfiguration;
import org.springframework.data.rest.core.Resource;
import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.ResourcePostProcessor;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaDialect;
@@ -66,11 +70,6 @@ public class ApplicationConfig {
return txManager;
}
@Bean public RepositoryRestConfiguration repositoryRestConfiguration() {
return new RepositoryRestConfiguration()
.setJsonpOnErrParamName("errback");
}
@Bean public TestRepositoryEventListener testRepositoryEventListener() {
return new TestRepositoryEventListener();
}
@@ -89,4 +88,15 @@ public class ApplicationConfig {
return cs;
}
@Bean public Map<Class<?>, ResourcePostProcessor> resourcePostProcessors() {
Map<Class<?>, ResourcePostProcessor> resourcePostProcessors = new HashMap<Class<?>, ResourcePostProcessor>();
resourcePostProcessors.put(Person.class, new ResourcePostProcessor() {
@Override public Resource postProcess(ServerHttpRequest request, Resource r) {
System.out.println(" **** post-processing request: " + request + " with resource: " + r);
return r;
}
});
return resourcePostProcessors;
}
}

View File

@@ -0,0 +1,21 @@
package org.springframework.data.rest.test.webmvc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.rest.core.Resource;
import org.springframework.data.rest.webmvc.ResourcePostProcessor;
import org.springframework.http.server.ServerHttpRequest;
/**
* @author Jon Brisbin
*/
public class LoggingResourcePostProcessor implements ResourcePostProcessor {
private Logger logger = LoggerFactory.getLogger(LoggingResourcePostProcessor.class);
@Override public Resource postProcess(ServerHttpRequest request, Resource r) {
logger.info(" **** post-processing request: " + request + " with resource: " + r);
return r;
}
}

View File

@@ -8,7 +8,21 @@
<bean id="config" class="org.springframework.data.rest.webmvc.RepositoryRestConfiguration"
p:jsonpParamName="callback"
p:jsonpOnErrParamName="errback"/>
p:jsonpOnErrParamName="errback">
<property name="domainTypeToRepositoryMappings">
<map key-type="java.lang.Class" value-type="java.lang.Class">
<entry key="org.springframework.data.rest.test.webmvc.Person"
value="org.springframework.data.rest.test.webmvc.PersonRepository"/>
</map>
</property>
<property name="resourcePostProcessors">
<map key-type="java.lang.Class">
<entry key="org.springframework.data.rest.test.webmvc.Person">
<bean class="org.springframework.data.rest.test.webmvc.LoggingResourcePostProcessor"/>
</entry>
</map>
</property>
</bean>
<!--
If you need to add Converters to the REST exporter to handle the property types you're using