diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/LinkAware.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/LinkAware.java new file mode 100644 index 000000000..06a67b829 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/LinkAware.java @@ -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> { + + @JsonProperty("links") + protected List links = new ArrayList(); + + public List getLinks() { + return links; + } + + @SuppressWarnings({"unchecked"}) + public T setLinks(List 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; + } + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java index 980995cab..ddaeb9015 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java @@ -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 */ @@ -17,7 +15,6 @@ public class Links { return this; } - @JsonProperty("_links") public List getLinks() { return this.links; } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/MapResource.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/MapResource.java new file mode 100644 index 000000000..5f8edc267 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/MapResource.java @@ -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> { + + public MapResource() { + } + + public MapResource(Map resource) { + this.resource = resource; + } + + @Override + @JsonIgnore + public Map getResource() { + return resource; + } + + @JsonAnyGetter + public Map any() { + return resource; + } + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/PostProcessor.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/PostProcessor.java new file mode 100644 index 000000000..a472bf6dd --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/PostProcessor.java @@ -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 { + + /** + * Possibly perform some mutation on the object as a post-processing step in a flow. + * + * @param obj + */ + T postProcess(T obj); + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resolver.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resolver.java new file mode 100644 index 000000000..1dc6f3ae1 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resolver.java @@ -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 { + + /** + * 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); + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resource.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resource.java new file mode 100644 index 000000000..e699d52b7 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resource.java @@ -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 extends LinkAware> { + + @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; + } + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resources.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resources.java new file mode 100644 index 000000000..5706d67bd --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resources.java @@ -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 { + + @JsonProperty("content") + protected List> resources = new ArrayList>(); + + 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() : resource)); + return this; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResources.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResources.java new file mode 100644 index 000000000..b8573ca7c --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResources.java @@ -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; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java new file mode 100644 index 000000000..dee278ea7 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java @@ -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; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadataResolver.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadataResolver.java new file mode 100644 index 000000000..92e21b719 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadataResolver.java @@ -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, E extends EntityMetadata> + implements Resolver, + ApplicationContextAware { + + private ApplicationContext applicationContext; + @Autowired(required = false) + private List repositoryExporters = Collections.emptyList(); + + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public List getRepositoryExporters() { + return repositoryExporters; + } + + public RepositoryMetadataResolver setRepositoryExporters(List 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; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java index d05c5e671..8200a37a6 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java @@ -18,7 +18,7 @@ public class RepositoryMethodResponse { @JsonProperty("results") private List results = new ArrayList(); - @JsonProperty("_links") + @JsonProperty("links") private List links = new ArrayList(); private long totalCount = 0; private int totalPages = 1; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java index ca05be56f..01117e9fe 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java @@ -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> 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> customConverters = Collections.emptyList(); + private Multimap, ResourcePostProcessor> resourcePostProcessors = ArrayListMultimap.create(); + private List 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 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 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 getResourcePostProcessors(Class type) { + Collection pps = resourcePostProcessors.get(type); + if(null == pps) { + return Collections.emptyList(); + } else { + return pps; + } + } + } 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 ac3ed38db..9c01b822b 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 @@ -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 availableMediaTypes = new TreeSet(); + private SortedSet availableMediaTypes = new TreeSet(); @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)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 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 entry : ((Map)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 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 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 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)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 entry : ((Map)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)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)linkedRepoMeta.entityMetadata() @@ -1224,18 +1266,18 @@ public class RepositoryRestController return notFoundResponse(request); } - Map 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 entityMetadata, + URI baseUri) { + Map entityDto = new HashMap(); + MapResource resource = new MapResource(entityDto); + + for(Map.Entry 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 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 void publishEvent(E event) { if(null != applicationContext) { applicationContext.publishEvent(event); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestExporterServlet.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestExporterServlet.java index e9cf49107..07b9aeb0a 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestExporterServlet.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestExporterServlet.java @@ -12,6 +12,8 @@ import org.springframework.web.servlet.DispatcherServlet; */ public class RepositoryRestExporterServlet extends DispatcherServlet { + private static final long serialVersionUID = 1L; + public RepositoryRestExporterServlet() { configure(); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java index 7a4525356..844a56467 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java @@ -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.asList( + resourcesReturnValueHandler + )); } @Override public int getOrder() { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java index cf1d43f29..64c5bd863 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java @@ -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. diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcePostProcessor.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcePostProcessor.java new file mode 100644 index 000000000..222ec7cca --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcePostProcessor.java @@ -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 { +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcesReturnValueHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcesReturnValueHandler.java new file mode 100644 index 000000000..3743bfc77 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourcesReturnValueHandler.java @@ -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 + 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; + + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResponsePostProcessor.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResponsePostProcessor.java new file mode 100644 index 000000000..154b2a03d --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResponsePostProcessor.java @@ -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 { +} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy index 44b36662e..3bf1a6821 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy @@ -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 } diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/QueryMethodsSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/QueryMethodsSpec.groovy index b12d9142e..9f20c0987 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/QueryMethodsSpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/QueryMethodsSpec.groovy @@ -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 }