Changing the representation to be more consistent in key naming (breaking change). Also starting work on the ability to "enrich" a representation with e.g. more links to other resources, etc...

This commit is contained in:
Jon Brisbin
2012-08-06 12:25:26 -05:00
parent bd24e582fd
commit 7775af8be4
21 changed files with 646 additions and 125 deletions

View File

@@ -0,0 +1,39 @@
package org.springframework.data.rest.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.codehaus.jackson.annotate.JsonProperty;
import org.springframework.util.Assert;
/**
* @author Jon Brisbin
*/
public abstract class LinkAware<T extends LinkAware<? super T>> {
@JsonProperty("links")
protected List<Link> links = new ArrayList<Link>();
public List<Link> getLinks() {
return links;
}
@SuppressWarnings({"unchecked"})
public T setLinks(List<Link> links) {
if(null == links) {
this.links = Collections.emptyList();
} else {
this.links = links;
}
return (T)this;
}
@SuppressWarnings({"unchecked"})
public T addLink(Link link) {
Assert.notNull(link, "Link cannot be null!");
links.add(link);
return (T)this;
}
}

View File

@@ -3,8 +3,6 @@ package org.springframework.data.rest.core;
import java.util.ArrayList;
import java.util.List;
import org.codehaus.jackson.annotate.JsonProperty;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@@ -17,7 +15,6 @@ public class Links {
return this;
}
@JsonProperty("_links")
public List<Link> getLinks() {
return this.links;
}

View File

@@ -0,0 +1,33 @@
package org.springframework.data.rest.core;
import java.util.Map;
import org.codehaus.jackson.annotate.JsonAnyGetter;
import org.codehaus.jackson.annotate.JsonIgnore;
/**
* Abstraction that represents a Map-like REST resource plus a set of links.
*
* @author Jon Brisbin
*/
public class MapResource extends Resource<Map<String, Object>> {
public MapResource() {
}
public MapResource(Map<String, Object> resource) {
this.resource = resource;
}
@Override
@JsonIgnore
public Map<String, Object> getResource() {
return resource;
}
@JsonAnyGetter
public Map<String, Object> any() {
return resource;
}
}

View File

@@ -0,0 +1,18 @@
package org.springframework.data.rest.core;
/**
* Implementations of this interface will post-process objects to mutate them in ways meaningful to the context in
* which they are called.
*
* @author Jon Brisbin
*/
public interface PostProcessor<T> {
/**
* Possibly perform some mutation on the object as a post-processing step in a flow.
*
* @param obj
*/
T postProcess(T obj);
}

View File

@@ -0,0 +1,24 @@
package org.springframework.data.rest.core;
import java.net.URI;
/**
* Implementations of this interface are responsible for turning {@link URI}s into real objects.
*
* @author Jon Brisbin
*/
public interface Resolver<T> {
/**
* Take a {@link URI} and resolve it to an actual object.
*
* @param baseUri
* The base URI that this resource is relative to.
* @param uri
* The URI id of the resource.
*
* @return The resolved object or {@literal null} if not found.
*/
T resolve(URI baseUri, URI uri);
}

View File

@@ -0,0 +1,31 @@
package org.springframework.data.rest.core;
import org.codehaus.jackson.annotate.JsonPropertyOrder;
import org.codehaus.jackson.annotate.JsonUnwrapped;
/**
* Wraps a simple object or a bean plus a set of links.
*
* @author Jon Brisbin
*/
public class Resource<T> extends LinkAware<Resource<T>> {
@JsonUnwrapped
protected T resource;
public Resource() {
}
public Resource(T resource) {
this.resource = resource;
}
public T getResource() {
return resource;
}
public void setResource(T resource) {
this.resource = resource;
}
}

View File

@@ -0,0 +1,38 @@
package org.springframework.data.rest.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.codehaus.jackson.annotate.JsonProperty;
/**
* Abstraction for representing resources to the user agent.
*
* @author Jon Brisbin
*/
@SuppressWarnings({"unchecked"})
public class Resources extends LinkAware<Resources> {
@JsonProperty("content")
protected List<Resource<?>> resources = new ArrayList<Resource<?>>();
public List getResources() {
return resources;
}
public Resources setResources(List resources) {
if(null == resources) {
this.resources = Collections.emptyList();
} else {
this.resources = resources;
}
return this;
}
public Resources addResource(Resource<?> resource) {
resources.add((null == resource ? new Resource<Object>() : resource));
return this;
}
}

View File

@@ -0,0 +1,33 @@
package org.springframework.data.rest.repository;
import org.codehaus.jackson.annotate.JsonProperty;
import org.springframework.data.rest.core.Resources;
/**
* @author Jon Brisbin
*/
public class PageableResources extends Resources {
protected long resourceCount = 0;
@JsonProperty("page")
protected PagingMetadata paging = new PagingMetadata(-1, 0);
public long getResourceCount() {
return resourceCount;
}
public PageableResources setResourceCount(long resourceCount) {
this.resourceCount = resourceCount;
return this;
}
public PagingMetadata getPaging() {
return paging;
}
public PageableResources setPaging(PagingMetadata paging) {
this.paging = paging;
return this;
}
}

View File

@@ -0,0 +1,34 @@
package org.springframework.data.rest.repository;
/**
* @author Jon Brisbin
*/
public class PagingMetadata {
private int current = 0;
private int total = 0;
public PagingMetadata(int current, int total) {
this.current = current;
this.total = total;
}
public int getCurrent() {
return current;
}
public PagingMetadata setCurrent(int current) {
this.current = current;
return this;
}
public int getTotal() {
return total;
}
public PagingMetadata setTotal(int total) {
this.total = total;
return this;
}
}

View File

@@ -0,0 +1,59 @@
package org.springframework.data.rest.repository;
import static org.springframework.data.rest.core.util.UriUtils.*;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.rest.core.Resolver;
import org.springframework.util.Assert;
/**
* @author Jon Brisbin
*/
public class RepositoryMetadataResolver<M extends RepositoryMetadata<E>, E extends EntityMetadata<? extends AttributeMetadata>>
implements Resolver<M>,
ApplicationContextAware {
private ApplicationContext applicationContext;
@Autowired(required = false)
private List<RepositoryExporter> repositoryExporters = Collections.emptyList();
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public List<RepositoryExporter> getRepositoryExporters() {
return repositoryExporters;
}
public RepositoryMetadataResolver<M, E> setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
Assert.notNull(repositoryExporters, "List of RepositoryExporters cannot be null!");
this.repositoryExporters = repositoryExporters;
return this;
}
@SuppressWarnings({"unchecked"})
@Override public M resolve(URI baseUri, URI uri) {
URI tail;
if(null == (tail = tail(baseUri, uri))) {
return null;
}
String path = tail.getPath();
for(RepositoryExporter exporter : repositoryExporters) {
RepositoryMetadata repoMeta;
if(null != (repoMeta = exporter.repositoryMetadataFor(path))) {
return (M)repoMeta;
}
}
return null;
}
}

View File

@@ -18,7 +18,7 @@ public class RepositoryMethodResponse {
@JsonProperty("results")
private List<Object> results = new ArrayList<Object>();
@JsonProperty("_links")
@JsonProperty("links")
private List<Link> links = new ArrayList<Link>();
private long totalCount = 0;
private int totalPages = 1;

View File

@@ -1,8 +1,11 @@
package org.springframework.data.rest.webmvc;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
@@ -17,15 +20,17 @@ 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 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<ResponsePostProcessor> responsePostProcessors = Collections.emptyList();
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.
@@ -226,4 +231,52 @@ public class RepositoryRestConfiguration {
return this;
}
/**
* Get the list of {@link ResponsePostProcessor}s that will potentially alter the responses going back to the
* client.
*
* @return
*/
public List<ResponsePostProcessor> getResponsePostProcessors() {
return responsePostProcessors;
}
/**
* Set the list of {@link ResponsePostProcessor}s that will potentially alter the responses going back to the
*
* @param responsePostProcessors
*/
public void setResponsePostProcessors(List<ResponsePostProcessor> responsePostProcessors) {
this.responsePostProcessors = responsePostProcessors;
}
/**
* Add a {@link ResourcePostProcessor} that is responsible for post-processing a particular domain type.
*
* @param type
* @param postProcessor
*
* @return
*/
public RepositoryRestConfiguration addResourcePostProcessor(Class<?> type, ResourcePostProcessor postProcessor) {
resourcePostProcessors.put(type, postProcessor);
return this;
}
/**
* Get the {@link ResourcePostProcessor}s assigned to a particular domain type.
*
* @param type
*
* @return
*/
public Collection<ResourcePostProcessor> getResourcePostProcessors(Class<?> type) {
Collection<ResourcePostProcessor> pps = resourcePostProcessors.get(type);
if(null == pps) {
return Collections.emptyList();
} else {
return pps;
}
}
}

View File

@@ -42,11 +42,16 @@ import org.springframework.data.repository.Repository;
import org.springframework.data.rest.core.Handler;
import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.Links;
import org.springframework.data.rest.core.MapResource;
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.core.convert.DelegatingConversionService;
import org.springframework.data.rest.core.util.UriUtils;
import org.springframework.data.rest.repository.AttributeMetadata;
import org.springframework.data.rest.repository.EntityMetadata;
import org.springframework.data.rest.repository.PageableResources;
import org.springframework.data.rest.repository.PagingMetadata;
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
@@ -63,7 +68,6 @@ import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent;
import org.springframework.data.rest.repository.context.BeforeSaveEvent;
import org.springframework.data.rest.repository.context.RepositoryEvent;
import org.springframework.data.rest.repository.invoke.CrudMethod;
import org.springframework.data.rest.repository.invoke.RepositoryMethodResponse;
import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.http.HttpHeaders;
@@ -151,7 +155,7 @@ public class RepositoryRestController
/**
* List of {@link MediaType}s we can support, given the list of {@link HttpMessageConverter}s currently configured.
*/
private SortedSet<MediaType> availableMediaTypes = new TreeSet<MediaType>();
private SortedSet<String> availableMediaTypes = new TreeSet<String>();
@Autowired(required = false)
private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT;
private ObjectMapper objectMapper = new ObjectMapper();
@@ -312,17 +316,17 @@ public class RepositoryRestController
UriComponentsBuilder uriBuilder) throws IOException {
URI baseUri = uriBuilder.build().toUri();
Links links = new Links();
Resources resources = new Resources();
for(RepositoryExporter repoExporter : repositoryExporters) {
for(String name : (Set<String>)repoExporter.repositoryNames()) {
RepositoryMetadata repoMeta = repoExporter.repositoryMetadataFor(name);
String rel = repoMeta.rel();
URI path = buildUri(baseUri, name);
links.add(new SimpleLink(rel, path));
resources.addLink(new SimpleLink(rel, path));
}
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), links);
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
/**
@@ -356,19 +360,19 @@ public class RepositoryRestController
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
RepositoryMethodResponse response = new RepositoryMethodResponse();
Iterator allEntities = Collections.emptyList().iterator();
final Resources resources;
if(repoMeta.repository() instanceof PagingAndSortingRepository) {
PageableResources pr = new PageableResources();
Page page = ((PagingAndSortingRepository)repoMeta.repository()).findAll(pageSort);
if(page.hasContent()) {
allEntities = page.iterator();
}
// Set page counts in the response
response.setTotalCount(page.getTotalElements());
response.setTotalPages(page.getTotalPages());
response.setCurrentPage(page.getNumber() + 1);
pr.setPaging(new PagingMetadata(page.getNumber() + 1, page.getTotalPages()));
pr.setResourceCount(page.getTotalElements());
// Copy over parameters
UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository);
@@ -388,7 +392,7 @@ public class RepositoryRestController
!page.isFirstPage() && page.hasPreviousPage(),
page.getNumber(),
"prev",
response.getLinks()
pr.getLinks()
);
maybeAddPrevNextLink(
nextPrevBase,
@@ -398,35 +402,43 @@ public class RepositoryRestController
!page.isLastPage() && page.hasNextPage(),
page.getNumber() + 2,
"next",
response.getLinks()
pr.getLinks()
);
resources = pr;
} else {
Iterable it = repoMeta.repository().findAll();
if(null != it) {
allEntities = it.iterator();
}
resources = new Resources();
}
while(allEntities.hasNext()) {
Object o = allEntities.next();
Serializable id = (Serializable)repoMeta.entityMetadata().idAttribute().get(o);
if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) {
response.addLink(new SimpleLink(repoMeta.rel() + "." + o.getClass().getSimpleName(),
buildUri(baseUri, repository, id.toString())));
resources.addLink(new SimpleLink(repoMeta.rel() + "." + o.getClass().getSimpleName(),
buildUri(baseUri, repository, id.toString())));
} else {
Map<String, Object> entityDto = extractPropertiesLinkAware(repoMeta.rel(),
o,
repoMeta.entityMetadata(),
buildUri(baseUri, repository, id.toString()));
addSelfLink(baseUri, entityDto, repository, id.toString());
response.addResult(entityDto);
MapResource res = createResource(repoMeta.rel(),
o,
repoMeta.entityMetadata(),
buildUri(baseUri, repository, id.toString()));
URI selfUri = buildUri(baseUri, repository, id.toString());
res.addLink(new SimpleLink(SELF, selfUri));
resources.addResource(res);
}
}
response.addLink(new SimpleLink(repoMeta.rel() + ".search",
buildUri(baseUri, repository, "search")));
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), response);
if(!repoMeta.queryMethods().isEmpty()) {
resources.addLink(new SimpleLink(repoMeta.rel() + ".search",
buildUri(baseUri, repository, "search")));
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
/**
@@ -452,7 +464,7 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Links links = new Links();
Resources resources = new Resources();
for(Map.Entry<String, RepositoryQueryMethod> entry : ((Map<String, RepositoryQueryMethod>)repoMeta.queryMethods())
.entrySet()) {
@@ -462,7 +474,7 @@ public class RepositoryRestController
Method m = entry.getValue().method();
if(m.isAnnotationPresent(RestResource.class)) {
RestResource resourceAnno = m.getAnnotation(RestResource.class);
links.add(new SimpleLink(
resources.addLink(new SimpleLink(
(StringUtils.hasText(resourceAnno.rel())
? repoMeta.rel() + "." + resourceAnno.rel()
: repoMeta.rel() + "." + entry.getKey()),
@@ -473,12 +485,12 @@ public class RepositoryRestController
));
} else {
// No customizations, use the default
links.add(new SimpleLink(repoMeta.rel() + "." + entry.getKey(),
buildUri(baseSearchUri, entry.getKey())));
resources.addLink(new SimpleLink(repoMeta.rel() + "." + entry.getKey(),
buildUri(baseSearchUri, entry.getKey())));
}
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), links);
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
/**
@@ -567,17 +579,15 @@ public class RepositoryRestController
}
}
RepositoryMethodResponse response = new RepositoryMethodResponse();
Object result;
if(null == (result = queryMethod.method().invoke(repo, paramVals))) {
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), response);
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), new Resources());
}
Resources resources = new Resources();
Iterator entities = Collections.emptyList().iterator();
if(result instanceof Collection) {
entities = ((Collection)result).iterator();
response.setTotalCount(((Collection)result).size());
} else if(result instanceof Page) {
Page page = (Page)result;
@@ -586,9 +596,9 @@ public class RepositoryRestController
}
// Set page counts in the response
response.setTotalCount(page.getTotalElements());
response.setTotalPages(page.getTotalPages());
response.setCurrentPage(page.getNumber() + 1);
PageableResources pr = new PageableResources();
pr.setResourceCount(page.getTotalElements());
pr.setPaging(new PagingMetadata(page.getNumber() + 1, page.getTotalPages()));
// Copy over parameters
UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository, "search", query);
@@ -608,7 +618,7 @@ public class RepositoryRestController
!page.isFirstPage() && page.hasPreviousPage(),
page.getNumber(),
"prev",
response.getLinks()
pr.getLinks()
);
maybeAddPrevNextLink(
nextPrevBase,
@@ -618,9 +628,10 @@ public class RepositoryRestController
!page.isLastPage() && page.hasNextPage(),
page.getNumber() + 2,
"next",
response.getLinks()
pr.getLinks()
);
resources = pr;
} else {
entities = Collections.singletonList(result).iterator();
}
@@ -630,7 +641,7 @@ public class RepositoryRestController
RepositoryMetadata elemRepoMeta;
if(null == (elemRepoMeta = repositoryMetadataFor(obj.getClass()))) {
response.addResult(obj);
resources.addResource(new Resource(obj));
continue;
}
@@ -639,18 +650,21 @@ public class RepositoryRestController
if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) {
String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName();
URI path = buildUri(baseUri, repository, id);
response.addLink(new SimpleLink(rel, path));
resources.addLink(new SimpleLink(rel, path));
} else {
Map<String, Object> entityDto = extractPropertiesLinkAware(repoMeta.rel(),
obj,
repoMeta.entityMetadata(),
buildUri(baseUri, repository, id));
addSelfLink(baseUri, entityDto, repository, id);
response.addResult(entityDto);
MapResource res = createResource(repoMeta.rel(),
obj,
repoMeta.entityMetadata(),
buildUri(baseUri, repository, id));
URI selfUri = buildUri(baseUri, repository, id);
res.addLink(new SimpleLink(SELF, selfUri));
resources.addResource(res);
}
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), response);
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
}
/**
@@ -702,15 +716,14 @@ public class RepositoryRestController
HttpHeaders headers = new HttpHeaders();
headers.set(LOCATION, selfUri.toString());
Object body = null;
if(null != request.getServletRequest().getParameter("returnBody")
&& "true".equals(request.getServletRequest().getParameter("returnBody"))) {
Map<String, Object> entityDto = extractPropertiesLinkAware(repoMeta.rel(),
savedEntity,
repoMeta.entityMetadata(),
buildUri(baseUri, repository, sId));
addSelfLink(baseUri, entityDto, repository, sId);
body = entityDto;
Resource<?> body = null;
if(returnBody(request)) {
MapResource resource = createResource(repoMeta.rel(),
savedEntity,
repoMeta.entityMetadata(),
buildUri(baseUri, repository, sId));
resource.addLink(new SimpleLink(SELF, selfUri));
body = resource;
}
return negotiateResponse(request, HttpStatus.CREATED, headers, body);
@@ -767,13 +780,14 @@ public class RepositoryRestController
headers.set("ETag", "\"" + version.toString() + "\"");
}
}
Map<String, Object> entityDto = extractPropertiesLinkAware(repoMeta.rel(),
entity,
repoMeta.entityMetadata(),
buildUri(baseUri, repository, id));
addSelfLink(baseUri, entityDto, repository, id);
MapResource res = createResource(repoMeta.rel(),
entity,
repoMeta.entityMetadata(),
baseUri);
URI selfUri = buildUri(baseUri, repository, id);
res.addLink(new SimpleLink(SELF, selfUri));
return negotiateResponse(request, HttpStatus.OK, headers, entityDto);
return negotiateResponse(request, HttpStatus.OK, headers, res);
}
/**
@@ -794,8 +808,7 @@ public class RepositoryRestController
@RequestMapping(
value = "/{repository}/{id}",
method = {
RequestMethod.PUT,
RequestMethod.POST
RequestMethod.PUT
}
)
@ResponseBody
@@ -818,51 +831,64 @@ public class RepositoryRestController
CrudRepository repo = repoMeta.repository();
Class<?> domainType = repoMeta.entityMetadata().type();
boolean returnBody = true;
if(null != request.getServletRequest().getParameter("returnBody")) {
returnBody = Boolean.parseBoolean(request.getServletRequest().getParameter("returnBody"));
}
MediaType incomingMediaType = request.getHeaders().getContentType();
Object incoming;
if(null == (incoming = readIncoming(request, incomingMediaType, domainType))) {
throw new HttpMessageNotReadableException("Could not create an instance of " + domainType.getSimpleName() + " from input.");
throw new HttpMessageNotReadableException("Could not create an instance of "
+ domainType.getSimpleName() + " from input.");
}
// Set the ID specified in the URL
repoMeta.entityMetadata().idAttribute().set(serId, incoming);
if(request.getMethod() == HttpMethod.POST) {
publishEvent(new BeforeSaveEvent(incoming));
Object savedEntity = repo.save(incoming);
publishEvent(new AfterSaveEvent(savedEntity));
URI selfUri = buildUri(baseUri, repository, id);
HttpHeaders headers = new HttpHeaders();
headers.set(LOCATION, selfUri.toString());
return negotiateResponse(request, HttpStatus.CREATED, headers, (returnBody ? savedEntity : null));
} else {
Object entity;
if(null == (entity = repo.findOne(serId))) {
return notFoundResponse(request);
}
boolean isUpdate = false;
Object entity;
if(null != (entity = repo.findOne(serId))) {
// Updating an existing resource
isUpdate = true;
for(AttributeMetadata attrMeta : (Collection<AttributeMetadata>)repoMeta.entityMetadata()
.embeddedAttributes()
.values()) {
Object incomingVal = attrMeta.get(incoming);
if(null != incomingVal) {
Object incomingVal;
if(null != (incomingVal = attrMeta.get(incoming))) {
attrMeta.set(incomingVal, entity);
}
}
publishEvent(new BeforeSaveEvent(entity));
Object savedEntity = repo.save(entity);
publishEvent(new AfterSaveEvent(savedEntity));
return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), (returnBody ? savedEntity : null));
} else {
entity = incoming;
}
publishEvent(new BeforeSaveEvent(entity));
Object savedEntity = repo.save(entity);
publishEvent(new AfterSaveEvent(savedEntity));
URI selfUri = buildUri(baseUri, repository, id);
Object body = null;
if(returnBody(request)) {
MapResource res = createResource(repoMeta.rel(),
savedEntity,
repoMeta.entityMetadata(),
baseUri);
res.addLink(new SimpleLink(SELF, selfUri));
body = res;
}
if(!isUpdate) {
HttpHeaders headers = new HttpHeaders();
headers.set(LOCATION, selfUri.toString());
return negotiateResponse(request,
HttpStatus.CREATED,
headers,
body);
} else {
return negotiateResponse(request,
(null != body ? HttpStatus.OK : HttpStatus.NO_CONTENT),
new HttpHeaders(),
body);
}
}
/**
@@ -968,8 +994,8 @@ public class RepositoryRestController
return notFoundResponse(request);
}
Resources res = new Resources();
AttributeMetadata idAttr = propRepoMeta.entityMetadata().idAttribute();
Links links = new Links();
if(propVal instanceof Collection) {
for(Object o : (Collection)propVal) {
String propValId = idAttr.get(o).toString();
@@ -977,7 +1003,7 @@ public class RepositoryRestController
+ entity.getClass().getSimpleName() + "."
+ attrType.getSimpleName();
URI path = buildUri(baseUri, repository, id, property, propValId);
links.add(new SimpleLink(rel, path));
res.addLink(new SimpleLink(rel, path));
}
} else if(propVal instanceof Map) {
for(Map.Entry<Object, Object> entry : ((Map<Object, Object>)propVal).entrySet()) {
@@ -990,16 +1016,16 @@ public class RepositoryRestController
} else {
sKey = conversionService.convert(oKey, String.class);
}
links.add(new SimpleLink(sKey, path));
res.addLink(new SimpleLink(sKey, path));
}
} else {
String propValId = idAttr.get(propVal).toString();
String rel = repository + "." + entity.getClass().getSimpleName() + "." + property;
URI path = buildUri(baseUri, repository, id, property, propValId);
links.add(new SimpleLink(rel, path));
res.addLink(new SimpleLink(rel, path));
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), links);
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), res);
}
/**
@@ -1203,17 +1229,33 @@ public class RepositoryRestController
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
// Check for the existence of the parent
CrudRepository repo = repoMeta.repository();
Serializable serId = stringToSerializable(id,
(Class<? extends Serializable>)repoMeta.entityMetadata()
.idAttribute()
.type());
if(!repo.exists(serId)) {
return notFoundResponse(request);
}
// Check for the existence of the property
AttributeMetadata attrMeta;
if(null == (attrMeta = repoMeta.entityMetadata().attribute(property))) {
return notFoundResponse(request);
}
// Find linked entity
// Check for the existence of a Repository for the linked entity
RepositoryMetadata linkedRepoMeta;
if(null == (linkedRepoMeta = repositoryMetadataFor(attrMeta))) {
return notFoundResponse(request);
}
if(!linkedRepoMeta.exportsMethod(CrudMethod.FIND_ONE)) {
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
// Find the linked entity
CrudRepository linkedRepo = linkedRepoMeta.repository();
Serializable sChildId = stringToSerializable(linkedId,
(Class<? extends Serializable>)linkedRepoMeta.entityMetadata()
@@ -1224,18 +1266,18 @@ public class RepositoryRestController
return notFoundResponse(request);
}
Map<String, Object> entityDto = extractPropertiesLinkAware(linkedRepoMeta.rel(),
linkedEntity,
linkedRepoMeta.entityMetadata(),
buildUri(baseUri,
linkedRepoMeta.name(),
linkedId));
URI selfUri = addSelfLink(baseUri, entityDto, linkedRepoMeta.name(), linkedId);
MapResource res = createResource(linkedRepoMeta.rel(),
linkedEntity,
linkedRepoMeta.entityMetadata(),
baseUri);
URI selfUri = buildUri(baseUri, linkedRepoMeta.name(), linkedId);
res.addLink(new SimpleLink(SELF, selfUri));
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Location", selfUri.toString());
return negotiateResponse(request, HttpStatus.OK, headers, entityDto);
return negotiateResponse(request, HttpStatus.OK, headers, res);
}
/**
@@ -1564,6 +1606,30 @@ public class RepositoryRestController
return null;
}
private MapResource createResource(String repoRel,
Object entity,
EntityMetadata<AttributeMetadata> entityMetadata,
URI baseUri) {
Map<String, Object> entityDto = new HashMap<String, Object>();
MapResource resource = new MapResource(entityDto);
for(Map.Entry<String, AttributeMetadata> attrMeta : entityMetadata.embeddedAttributes().entrySet()) {
String name = attrMeta.getKey();
Object val;
if(null != (val = attrMeta.getValue().get(entity))) {
entityDto.put(name, val);
}
}
for(String attrName : entityMetadata.linkedAttributes().keySet()) {
URI uri = buildUri(baseUri, attrName);
resource.addLink(new SimpleLink(repoRel + "." + entity.getClass().getSimpleName() + "." + attrName, uri));
}
return resource;
}
/*
@SuppressWarnings({"unchecked"})
private Map<String, Object> extractPropertiesLinkAware(String repoRel,
Object entity,
@@ -1592,6 +1658,7 @@ public class RepositoryRestController
return entityDto;
}
*/
private boolean shouldReturnLinks(String acceptHeader) {
if(null != acceptHeader) {
@@ -1609,6 +1676,15 @@ public class RepositoryRestController
return false;
}
private boolean returnBody(ServletServerHttpRequest request) {
String s = request.getServletRequest().getParameter("returnBody");
if(null != s) {
return "true".equals(s);
} else {
return false;
}
}
private <E extends RepositoryEvent> void publishEvent(E event) {
if(null != applicationContext) {
applicationContext.publishEvent(event);

View File

@@ -12,6 +12,8 @@ import org.springframework.web.servlet.DispatcherServlet;
*/
public class RepositoryRestExporterServlet extends DispatcherServlet {
private static final long serialVersionUID = 1L;
public RepositoryRestExporterServlet() {
configure();
}

View File

@@ -2,8 +2,10 @@ package org.springframework.data.rest.webmvc;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
/**
@@ -16,11 +18,17 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
*/
public class RepositoryRestHandlerAdapter extends RequestMappingHandlerAdapter {
@Autowired
private ResourcesReturnValueHandler resourcesReturnValueHandler;
public RepositoryRestHandlerAdapter(RepositoryRestConfiguration config) {
setCustomArgumentResolvers(Arrays.asList(
new ServerHttpRequestMethodArgumentResolver(),
new PagingAndSortingMethodArgumentResolver(config)
));
setCustomReturnValueHandlers(Arrays.<HandlerMethodReturnValueHandler>asList(
resourcesReturnValueHandler
));
}
@Override public int getOrder() {

View File

@@ -92,6 +92,10 @@ public class RepositoryRestMvcConfiguration {
return new RepositoryRestController();
}
@Bean ResourcesReturnValueHandler resourcesReturnValueHandler() {
return new ResourcesReturnValueHandler(repositoryRestConfig);
}
/**
* Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the
* {@link RepositoryRestController} class.

View File

@@ -0,0 +1,12 @@
package org.springframework.data.rest.webmvc;
import org.springframework.data.rest.core.PostProcessor;
import org.springframework.data.rest.core.Resource;
/**
* Implementations of this interface are allowed to mutate a {@link Resource} being sent back to the client.
*
* @author Jon Brisbin
*/
public interface ResourcePostProcessor extends PostProcessor<Resource> {
}

View File

@@ -0,0 +1,43 @@
package org.springframework.data.rest.webmvc;
import org.springframework.core.MethodParameter;
import org.springframework.data.rest.core.Resources;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* {@link HandlerMethodReturnValueHandler} implementation that applies user-defined post-processors to the
* representation being sent back to the client.
*
* @author Jon Brisbin
*/
public class ResourcesReturnValueHandler
extends RepositoryExporterSupport<ResourcesReturnValueHandler>
implements HandlerMethodReturnValueHandler {
private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT;
public ResourcesReturnValueHandler(RepositoryRestConfiguration config) {
if(null != config) {
this.config = config;
} else {
this.config = config;
}
}
@Override public boolean supportsReturnType(MethodParameter returnType) {
return Resources.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
Resources resources = (Resources)returnValue;
}
}

View File

@@ -0,0 +1,13 @@
package org.springframework.data.rest.webmvc;
import org.springframework.data.rest.core.PostProcessor;
import org.springframework.data.rest.core.Resources;
/**
* Implementations of this interface are allowed to mutate the {@link org.springframework.data.rest.core.Resources}
* object that is being sent back to the client as a response.
*
* @author Jon Brisbin
*/
public interface ResponsePostProcessor extends PostProcessor<Resources> {
}

View File

@@ -22,7 +22,7 @@ class DiscoverySpec extends BaseSpec {
response.statusCode == HttpStatus.OK
when:
def links = readJson(response)._links
def links = readJson(response).links
then:
links.size() == 4
@@ -38,9 +38,13 @@ class DiscoverySpec extends BaseSpec {
when:
def response = controller.listEntities(request, pageSort, baseUri, "people")
def body = readJson(response)
then:
response.statusCode == HttpStatus.OK
body.resources.size() == 10
body.page.total == 2
body.page.current == 1
}

View File

@@ -29,7 +29,7 @@ class QueryMethodsSpec extends BaseSpec {
then:
response.statusCode == HttpStatus.OK
body["_links"].size() == 2
body.links.size() == 2
}
@@ -47,7 +47,7 @@ class QueryMethodsSpec extends BaseSpec {
then:
response.statusCode == HttpStatus.OK
body["results"].size() == 1
body.resources.size() == 1
}