From 3393780e65adab149dbdcb8dc00684727cfbfbe2 Mon Sep 17 00:00:00 2001 From: Jon Brisbin Date: Mon, 13 Aug 2012 09:31:55 -0500 Subject: [PATCH] DATAREST-37: Display entities inline when accessing relationship links. --- .../springframework/data/rest/core/Link.java | 3 + .../data/rest/core/LinkAware.java | 39 ----- .../data/rest/core/LinkList.java | 36 +++++ .../springframework/data/rest/core/Links.java | 22 --- .../data/rest/core/Resource.java | 65 ++++++++- .../data/rest/core/ResourceLink.java | 60 ++++++++ .../data/rest/core/ResourceSet.java | 101 +++++++++++++ .../data/rest/core/Resources.java | 38 ----- .../data/rest/core/SimpleLink.java | 39 ----- .../core/util/FluentBeanDeserializer.java | 8 +- ...esources.java => PageableResourceSet.java} | 8 +- .../AbstractRepositoryEventListener.java | 4 +- .../rest/repository/context/RenderEvent.java | 13 +- .../repository/spec/ExtensionsSpec.groovy | 14 +- .../data/rest/webmvc/JacksonUtil.java | 5 +- .../rest/webmvc/RepositoryRestController.java | 137 ++++++++++++------ .../webmvc/ResourcesReturnValueHandler.java | 6 +- .../rest/webmvc/ResponsePostProcessor.java | 6 +- .../webmvc/UriListHttpMessageConverter.java | 35 +++-- .../spec/RepositoryRestControllerSpec.groovy | 5 +- .../src/test/resources/load_data.sh | 26 ++-- 21 files changed, 420 insertions(+), 250 deletions(-) delete mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/LinkAware.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/LinkList.java delete mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceLink.java create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceSet.java delete mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resources.java delete mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/SimpleLink.java rename spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/{PageableResources.java => PageableResourceSet.java} (68%) diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Link.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Link.java index 19df455c0..ba828fc33 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Link.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Link.java @@ -2,11 +2,14 @@ package org.springframework.data.rest.core; import java.net.URI; +import org.codehaus.jackson.annotate.JsonTypeInfo; + /** * A simple bean representing a URI link. * * @author Jon Brisbin */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, defaultImpl = ResourceLink.class) public interface Link { /** 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 deleted file mode 100644 index 06a67b829..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/LinkAware.java +++ /dev/null @@ -1,39 +0,0 @@ -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/LinkList.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/LinkList.java new file mode 100644 index 000000000..0698958f6 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/LinkList.java @@ -0,0 +1,36 @@ +package org.springframework.data.rest.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple abstraction for representing a list of {@link Link}s. + * + * @author Jon Brisbin + */ +public class LinkList { + + private List links = new ArrayList(); + + /** + * Add a {@link Link} to this list. + * + * @param link + * + * @return + */ + public LinkList add(Link link) { + links.add(link); + return this; + } + + /** + * Get the {@link Link}s in this list. + * + * @return + */ + public List getLinks() { + return this.links; + } + +} 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 deleted file mode 100644 index ddaeb9015..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.springframework.data.rest.core; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author Jon Brisbin - */ -public class Links { - - private List links = new ArrayList(); - - public Links add(Link link) { - links.add(link); - return this; - } - - public List getLinks() { - return this.links; - } - -} 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 index e699d52b7..b17b08c56 100644 --- 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 @@ -1,17 +1,24 @@ package org.springframework.data.rest.core; -import org.codehaus.jackson.annotate.JsonPropertyOrder; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonUnwrapped; +import org.springframework.util.Assert; /** * Wraps a simple object or a bean plus a set of links. * * @author Jon Brisbin */ -public class Resource extends LinkAware> { +public class Resource { @JsonUnwrapped protected T resource; + @JsonProperty("links") + protected Set links = new HashSet(); public Resource() { } @@ -20,12 +27,64 @@ public class Resource extends LinkAware> { this.resource = resource; } + /** + * Get the resource. May be {@literal null}. + * + * @return The resource or {@literal null}. + */ public T getResource() { return resource; } - public void setResource(T resource) { + /** + * Set the resource. + * + * @param resource + * + * @return {@literal this} + */ + public Resource setResource(T resource) { this.resource = resource; + return this; + } + + /** + * Get the set of {@link Link}s for this resource. + * + * @return + */ + public Set getLinks() { + return links; + } + + /** + * Set the entire set of {@link Link}s. + * + * @param links + * + * @return {@literal this} + */ + @SuppressWarnings({"unchecked"}) + public Resource setLinks(Set links) { + if(null == links) { + this.links = Collections.emptySet(); + } else { + this.links = links; + } + return this; + } + + /** + * Add a {@link Link} to this resource's set. + * + * @param link + * + * @return {@literal this} + */ + public Resource addLink(Link link) { + Assert.notNull(link, "Link cannot be null."); + links.add(link); + return this; } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceLink.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceLink.java new file mode 100644 index 000000000..804676705 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceLink.java @@ -0,0 +1,60 @@ +package org.springframework.data.rest.core; + +import java.net.URI; + +import org.codehaus.jackson.annotate.JsonIgnore; +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * Implementation of {@link Link}. + * + * @author Jon Brisbin + */ +public class ResourceLink implements Link, Comparable { + + @JsonProperty("rel") + private String rel; + @JsonProperty("href") + private URI href; + + public ResourceLink() { + } + + public ResourceLink(String rel, URI href) { + this.rel = rel; + this.href = href; + } + + @Override public String rel() { + return rel; + } + + @Override public URI href() { + return href; + } + + @Override public int compareTo(Link link) { + if(null == rel || null == link.rel()) { + return -1; + } + + int i = rel.compareTo(link.rel()); + if(i != 0) { + return i; + } + + if(null == href || null == link.href()) { + return -1; + } + + return (href.compareTo(link.href())); + } + + @Override public String toString() { + return "ResourceLink{" + + "rel='" + rel + '\'' + + ", href=" + href + + '}'; + } + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceSet.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceSet.java new file mode 100644 index 000000000..e62024103 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceSet.java @@ -0,0 +1,101 @@ +package org.springframework.data.rest.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.codehaus.jackson.annotate.JsonProperty; +import org.springframework.util.Assert; + +/** + * Abstraction for representing resources to the user agent. + * + * @author Jon Brisbin + */ +@SuppressWarnings({"unchecked"}) +public class ResourceSet { + + @JsonProperty("content") + protected List> resources = new ArrayList>(); + @JsonProperty("links") + protected Set links = new HashSet(); + + /** + * Get the {@link Resource}s this {@literal ResourceSet} manages. + * + * @return + */ + public List> getResources() { + return resources; + } + + /** + * Set the {@link Resource}s this {@literal ResourceSet} manages. + * + * @param resources + * + * @return + */ + public ResourceSet setResources(List> resources) { + if(null == resources) { + this.resources = Collections.emptyList(); + } else { + this.resources = resources; + } + return this; + } + + /** + * Add a {@link Resource} to this set. + * + * @param resource + * + * @return {@literal this} + */ + public ResourceSet addResource(Resource resource) { + resources.add((null == resource ? new Resource() : resource)); + return this; + } + + /** + * Get the set of {@link Link}s. + * + * @return + */ + public Set getLinks() { + return links; + } + + /** + * Set the {@link Link}s this {@literal ResourceSet} manages. + * + * @param links + * + * @return {@literal this} + */ + @SuppressWarnings({"unchecked"}) + public ResourceSet setLinks(Set links) { + if(null == links) { + this.links = Collections.emptySet(); + } else { + this.links = links; + } + return this; + } + + /** + * Add a {@link Link} to this resource's set. + * + * @param link + * + * @return {@literal this} + */ + public ResourceSet addLink(Link link) { + Assert.notNull(link, "Link cannot be null."); + links.add(link); + return this; + } + +} 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 deleted file mode 100644 index 5706d67bd..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Resources.java +++ /dev/null @@ -1,38 +0,0 @@ -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-core/src/main/java/org/springframework/data/rest/core/SimpleLink.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/SimpleLink.java deleted file mode 100644 index 2eac65899..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/SimpleLink.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.springframework.data.rest.core; - -import java.net.URI; - -/** - * Implementation of {@link Link}. - * - * @author Jon Brisbin - */ -public class SimpleLink - implements Link { - - private String rel; - private URI href; - - public SimpleLink() { - } - - public SimpleLink(String rel, URI href) { - this.rel = rel; - this.href = href; - } - - @Override public String rel() { - return rel; - } - - @Override public URI href() { - return href; - } - - @Override public String toString() { - return "SimpleLink{" + - "rel='" + rel + '\'' + - ", href=" + href + - '}'; - } - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java index c0a342e48..03a00822d 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java @@ -5,7 +5,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; import org.codehaus.jackson.JsonToken; import org.codehaus.jackson.map.DeserializationContext; import org.codehaus.jackson.map.deser.std.StdDeserializer; @@ -18,8 +17,7 @@ import org.springframework.util.ClassUtils; * * @author Jon Brisbin */ -public class FluentBeanDeserializer - extends StdDeserializer { +public class FluentBeanDeserializer extends StdDeserializer { private ConversionService conversionService; private FluentBeanUtils.Metadata beanMeta; @@ -37,9 +35,7 @@ public class FluentBeanDeserializer @Override public Object deserialize(JsonParser jp, - DeserializationContext ctxt) - throws IOException, - JsonProcessingException { + DeserializationContext ctxt) throws IOException { if(jp.getCurrentToken() != JsonToken.START_OBJECT) { throw ctxt.mappingException(_valueClass); } 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/PageableResourceSet.java similarity index 68% rename from spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResources.java rename to spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResourceSet.java index b8573ca7c..4f0b7ccfd 100644 --- 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/PageableResourceSet.java @@ -1,12 +1,12 @@ package org.springframework.data.rest.repository; import org.codehaus.jackson.annotate.JsonProperty; -import org.springframework.data.rest.core.Resources; +import org.springframework.data.rest.core.ResourceSet; /** * @author Jon Brisbin */ -public class PageableResources extends Resources { +public class PageableResourceSet extends ResourceSet { protected long resourceCount = 0; @JsonProperty("page") @@ -16,7 +16,7 @@ public class PageableResources extends Resources { return resourceCount; } - public PageableResources setResourceCount(long resourceCount) { + public PageableResourceSet setResourceCount(long resourceCount) { this.resourceCount = resourceCount; return this; } @@ -25,7 +25,7 @@ public class PageableResources extends Resources { return paging; } - public PageableResources setPaging(PagingMetadata paging) { + public PageableResourceSet setPaging(PagingMetadata paging) { this.paging = paging; return this; } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java index 6b9875c3a..45218e268 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java @@ -8,7 +8,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.data.rest.core.Resource; -import org.springframework.data.rest.core.Resources; +import org.springframework.data.rest.core.ResourceSet; import org.springframework.data.rest.repository.RepositoryExporter; import org.springframework.data.rest.repository.RepositoryExporterSupport; import org.springframework.data.rest.repository.RepositoryMetadata; @@ -143,7 +143,7 @@ public abstract class AbstractRepositoryEventListener)repoExporter.repositoryNames()) { RepositoryMetadata repoMeta = repoExporter.repositoryMetadataFor(name); String rel = repoMeta.rel(); URI path = buildUri(baseUri, name); - resources.addLink(new SimpleLink(rel, path)); + resources.addLink(new ResourceLink(rel, path)); } } @@ -368,9 +368,9 @@ public class RepositoryRestController } Iterator allEntities = Collections.emptyList().iterator(); - final Resources resources; + final ResourceSet resources; if(repoMeta.repository() instanceof PagingAndSortingRepository) { - PageableResources pr = new PageableResources(); + PageableResourceSet pr = new PageableResourceSet(); Page page = ((PagingAndSortingRepository)repoMeta.repository()).findAll(pageSort); if(page.hasContent()) { @@ -418,30 +418,30 @@ public class RepositoryRestController if(null != it) { allEntities = it.iterator(); } - resources = new Resources(); + resources = new ResourceSet(); } while(allEntities.hasNext()) { Object o = allEntities.next(); Serializable id = (Serializable)repoMeta.entityMetadata().idAttribute().get(o); if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) { - resources.addLink(new SimpleLink(repoMeta.rel() + "." + o.getClass().getSimpleName(), - buildUri(baseUri, repository, id.toString()))); + resources.addLink(new ResourceLink(repoMeta.rel() + "." + o.getClass().getSimpleName(), + buildUri(baseUri, repository, id.toString()))); } else { URI selfUri = buildUri(baseUri, repository, id.toString()); MapResource res = createResource(repoMeta.rel(), o, repoMeta.entityMetadata(), selfUri); - res.addLink(new SimpleLink(SELF, selfUri)); + res.addLink(new ResourceLink(SELF, selfUri)); resources.addResource(res); } } if(!repoMeta.queryMethods().isEmpty()) { - resources.addLink(new SimpleLink(repoMeta.rel() + ".search", - buildUri(baseUri, repository, "search"))); + resources.addLink(new ResourceLink(repoMeta.rel() + ".search", + buildUri(baseUri, repository, "search"))); } publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources)); @@ -472,7 +472,7 @@ public class RepositoryRestController URI baseUri = uriBuilder.build().toUri(); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - Resources resources = new Resources(); + ResourceSet resources = new ResourceSet(); for(Map.Entry entry : ((Map)repoMeta.queryMethods()) .entrySet()) { @@ -482,7 +482,7 @@ public class RepositoryRestController Method m = entry.getValue().method(); if(m.isAnnotationPresent(RestResource.class)) { RestResource resourceAnno = m.getAnnotation(RestResource.class); - resources.addLink(new SimpleLink( + resources.addLink(new ResourceLink( (StringUtils.hasText(resourceAnno.rel()) ? repoMeta.rel() + "." + resourceAnno.rel() : repoMeta.rel() + "." + entry.getKey()), @@ -493,8 +493,8 @@ public class RepositoryRestController )); } else { // No customizations, use the default - resources.addLink(new SimpleLink(repoMeta.rel() + "." + entry.getKey(), - buildUri(baseSearchUri, entry.getKey()))); + resources.addLink(new ResourceLink(repoMeta.rel() + "." + entry.getKey(), + buildUri(baseSearchUri, entry.getKey()))); } } @@ -608,10 +608,10 @@ public class RepositoryRestController Object result; if(null == (result = queryMethod.method().invoke(repo, paramVals))) { - return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), new Resources()); + return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), new ResourceSet()); } - Resources resources = new Resources(); + ResourceSet resources = new ResourceSet(); Iterator entities = Collections.emptyList().iterator(); if(result instanceof Collection) { entities = ((Collection)result).iterator(); @@ -623,7 +623,7 @@ public class RepositoryRestController } // Set page counts in the response - PageableResources pr = new PageableResources(); + PageableResourceSet pr = new PageableResourceSet(); pr.setResourceCount(page.getTotalElements()); pr.setPaging(new PagingMetadata(page.getNumber() + 1, page.getTotalPages())); @@ -677,14 +677,14 @@ public class RepositoryRestController if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) { String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName(); URI path = buildUri(baseUri, repository, id); - resources.addLink(new SimpleLink(rel, path)); + resources.addLink(new ResourceLink(rel, path)); } else { URI selfUri = buildUri(baseUri, repository, id); MapResource res = createResource(repoMeta.rel(), obj, repoMeta.entityMetadata(), selfUri); - res.addLink(new SimpleLink(SELF, selfUri)); + res.addLink(new ResourceLink(SELF, selfUri)); resources.addResource(res); } @@ -750,7 +750,7 @@ public class RepositoryRestController savedEntity, repoMeta.entityMetadata(), selfUri); - resource.addLink(new SimpleLink(SELF, selfUri)); + resource.addLink(new ResourceLink(SELF, selfUri)); body = resource; @@ -817,7 +817,7 @@ public class RepositoryRestController entity, repoMeta.entityMetadata(), selfUri); - res.addLink(new SimpleLink(SELF, selfUri)); + res.addLink(new ResourceLink(SELF, selfUri)); publishEvent(new BeforeRenderResourceEvent(request, repoMeta, res)); @@ -903,7 +903,7 @@ public class RepositoryRestController savedEntity, repoMeta.entityMetadata(), selfUri); - res.addLink(new SimpleLink(SELF, selfUri)); + res.addLink(new ResourceLink(SELF, selfUri)); body = res; @@ -993,6 +993,7 @@ public class RepositoryRestController @PathVariable String id, @PathVariable String property) throws IOException { URI baseUri = uriBuilder.build().toUri(); + String accept = request.getServletRequest().getHeader("Accept"); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) { @@ -1030,18 +1031,34 @@ public class RepositoryRestController return notFoundResponse(request); } - Resources res = new Resources(); + Object body; AttributeMetadata idAttr = propRepoMeta.entityMetadata().idAttribute(); + String propertyRel = repository + + "." + entity.getClass().getSimpleName() + + "." + property + + "." + propRepoMeta.entityMetadata().type().getSimpleName(); if(propVal instanceof Collection) { + ResourceSet resources = new ResourceSet(); for(Object o : (Collection)propVal) { String propValId = idAttr.get(o).toString(); - String rel = repository + "." - + entity.getClass().getSimpleName() + "." - + attrType.getSimpleName(); URI path = buildUri(baseUri, repository, id, property, propValId); - res.addLink(new SimpleLink(rel, path)); + + if(shouldReturnLinks(accept)) { + resources.addLink(new ResourceLink(propertyRel, path)); + } else { + URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId); + MapResource res = createResource(propRepoMeta.rel(), + o, + propRepoMeta.entityMetadata(), + selfUri); + res.addLink(new ResourceLink(SELF, selfUri)); + res.addLink(new ResourceLink(propertyRel, path)); + resources.addResource(res); + } } + body = resources; } else if(propVal instanceof Map) { + Map resource = new HashMap(); for(Map.Entry entry : ((Map)propVal).entrySet()) { String propValId = idAttr.get(entry.getValue()).toString(); URI path = buildUri(baseUri, repository, id, property, propValId); @@ -1052,18 +1069,43 @@ public class RepositoryRestController } else { sKey = conversionService.convert(oKey, String.class); } - res.addLink(new SimpleLink(sKey, path)); + + if(shouldReturnLinks(accept)) { + 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); + res.addLink(new ResourceLink(SELF, selfUri)); + res.addLink(new ResourceLink(propertyRel, path)); + resource.put(sKey, res); + } } + body = new MapResource(resource); } else { String propValId = idAttr.get(propVal).toString(); - String rel = repository + "." + entity.getClass().getSimpleName() + "." + property; URI path = buildUri(baseUri, repository, id, property, propValId); - res.addLink(new SimpleLink(rel, path)); + URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId); + if(shouldReturnLinks(accept)) { + Resource resource = new Resource(); + resource.addLink(new ResourceLink(propertyRel, path)); + body = resource; + } else { + MapResource res = createResource(propRepoMeta.rel(), + propVal, + propRepoMeta.entityMetadata(), + buildUri(baseUri, propRepoMeta.name(), propValId)); + res.addLink(new ResourceLink(propertyRel, path)); + res.addLink(new ResourceLink(SELF, selfUri)); + body = res; + } } - publishEvent(new BeforeRenderResourceEvent(request, propRepoMeta, res)); + publishEvent(new BeforeRenderResourceEvent(request, propRepoMeta, body)); - return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), res); + return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), body); } /** @@ -1140,11 +1182,7 @@ public class RepositoryRestController } String key = rel.get(); if(null == key) { - try { - return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), null); - } catch(IOException e) { - throw new RuntimeException(e); - } + throw new IllegalArgumentException("Map key cannot be null (usually the 'rel' value of a JSON object)."); } m.put(rel.get(), linkedEntity); attrMeta.set(m, entity); @@ -1157,10 +1195,13 @@ public class RepositoryRestController }; MediaType incomingMediaType = request.getHeaders().getContentType(); - Links incomingLinks = readIncoming(request, incomingMediaType, Links.class); + LinkList incomingLinks = readIncoming(request, + incomingMediaType, + LinkList.class); for(Link l : incomingLinks.getLinks()) { Object o; if(null != (o = resolveTopLevelResource(baseUri, l.href().toString()))) { + rel.set(l.rel()); ResponseEntity possibleResponse = entityHandler.handle(o); if(null != possibleResponse) { return possibleResponse; @@ -1304,12 +1345,14 @@ public class RepositoryRestController return notFoundResponse(request); } + 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); - res.addLink(new SimpleLink(SELF, selfUri)); + res.addLink(new ResourceLink(propertyRel, propertyPath)); publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, res)); @@ -1564,7 +1607,7 @@ public class RepositoryRestController model.put(LINKS, links); } URI selfUri = buildUri(baseUri, pathComponents); - links.add(new SimpleLink(SELF, selfUri)); + links.add(new ResourceLink(SELF, selfUri)); return selfUri; } @@ -1576,13 +1619,13 @@ public class RepositoryRestController boolean addIf, int nextPage, String rel, - List links) { + Set links) { if(null != page && addIf) { UriComponentsBuilder urib = UriComponentsBuilder.fromUri(resourceUri); urib.queryParam(config.getPageParamName(), nextPage); // PageRequest is 0-based, so it's already (page - 1) urib.queryParam(config.getLimitParamName(), page.getSize()); pageSort.addSortParameters(urib); - links.add(new SimpleLink(repoMeta.rel() + "." + rel, urib.build().toUri())); + links.add(new ResourceLink(repoMeta.rel() + "." + rel, urib.build().toUri())); } } @@ -1662,7 +1705,7 @@ public class RepositoryRestController for(String attrName : entityMetadata.linkedAttributes().keySet()) { URI uri = buildUri(baseUri, attrName); - resource.addLink(new SimpleLink(repoRel + "." + entity.getClass().getSimpleName() + "." + attrName, uri)); + resource.addLink(new ResourceLink(repoRel + "." + entity.getClass().getSimpleName() + "." + attrName, uri)); } return resource; 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 index 3743bfc77..29e304a8b 100644 --- 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 @@ -1,7 +1,7 @@ package org.springframework.data.rest.webmvc; import org.springframework.core.MethodParameter; -import org.springframework.data.rest.core.Resources; +import org.springframework.data.rest.core.ResourceSet; import org.springframework.data.rest.repository.RepositoryExporterSupport; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -28,7 +28,7 @@ public class ResourcesReturnValueHandler } @Override public boolean supportsReturnType(MethodParameter returnType) { - return Resources.class.isAssignableFrom(returnType.getParameterType()); + return ResourceSet.class.isAssignableFrom(returnType.getParameterType()); } @Override @@ -36,7 +36,7 @@ public class ResourcesReturnValueHandler MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { - Resources resources = (Resources)returnValue; + ResourceSet resources = (ResourceSet)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 index 154b2a03d..541177a78 100644 --- 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 @@ -1,13 +1,13 @@ package org.springframework.data.rest.webmvc; import org.springframework.data.rest.core.PostProcessor; -import org.springframework.data.rest.core.Resources; +import org.springframework.data.rest.core.ResourceSet; /** - * Implementations of this interface are allowed to mutate the {@link org.springframework.data.rest.core.Resources} + * Implementations of this interface are allowed to mutate the {@link org.springframework.data.rest.core.ResourceSet} * object that is being sent back to the client as a response. * * @author Jon Brisbin */ -public interface ResponsePostProcessor extends PostProcessor { +public interface ResponsePostProcessor extends PostProcessor { } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java index ca3d2426f..bb3309a75 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java @@ -10,8 +10,9 @@ import java.util.List; import java.util.Map; import org.springframework.data.rest.core.Link; -import org.springframework.data.rest.core.Links; -import org.springframework.data.rest.core.SimpleLink; +import org.springframework.data.rest.core.LinkList; +import org.springframework.data.rest.core.Resource; +import org.springframework.data.rest.core.ResourceLink; import org.springframework.data.rest.repository.invoke.RepositoryMethodResponse; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; @@ -36,7 +37,8 @@ public class UriListHttpMessageConverter extends AbstractHttpMessageConverter @@ -74,7 +75,7 @@ class RepositoryRestControllerSpec extends Specification { uriBuilder = UriComponentsBuilder.fromUriString("http://localhost:8080/data") def customSerializerFactory = new CustomSerializerFactory() - customSerializerFactory.addSpecificMapping(SimpleLink, new FluentBeanSerializer(SimpleLink)) + customSerializerFactory.addSpecificMapping(ResourceLink, new FluentBeanSerializer(ResourceLink)) mapper.setSerializerFactory(customSerializerFactory) } diff --git a/spring-data-rest-webmvc/src/test/resources/load_data.sh b/spring-data-rest-webmvc/src/test/resources/load_data.sh index 0c78835d7..ae4900941 100755 --- a/spring-data-rest-webmvc/src/test/resources/load_data.sh +++ b/spring-data-rest-webmvc/src/test/resources/load_data.sh @@ -1,15 +1,15 @@ #!/bin/sh -curl -d '{"surname" : "Doe"}' -H "Content-Type: application/json" http://localhost:8080/family -curl -d '{"name" : "John Doe"}' -H "Content-Type: application/json" http://localhost:8080/people -curl -d '{"name" : "Jane Doe"}' -H "Content-Type: application/json" http://localhost:8080/people -curl -d 'http://localhost:8080/people/1 +curl -v -d '{"surname" : "Doe"}' -H "Content-Type: application/json" http://localhost:8080/family +curl -v -d '{"name" : "John Doe"}' -H "Content-Type: application/json" http://localhost:8080/people +curl -v -d '{"name" : "Jane Doe"}' -H "Content-Type: application/json" http://localhost:8080/people +curl -v -d 'http://localhost:8080/people/1 http://localhost:8080/people/2' -H "Content-Type: text/uri-list" http://localhost:8080/family/1/members -curl -d '{"postalCode":"12345","province":"MO","lines":["1 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address -curl -d "http://localhost:8080/address/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/addresses -curl -d "http://localhost:8080/people/1" -X PUT -H "Content-Type: text/uri-list" http://localhost:8080/address/1/person -curl -d '{"postalCode":"54321","province":"MO","lines":["2 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address -curl -d "http://localhost:8080/address/2" -H "Content-Type: text/uri-list" http://localhost:8080/people/2/addresses -curl -d '{"type" : "twitter", "url": "#!/johndoe"}' -H "Content-Type: application/json" http://localhost:8080/profile -curl -d "http://localhost:8080/profile/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/profiles -curl -d '{"type" : "facebook", "url": "/janedoe"}' -H "Content-Type: application/json" http://localhost:8080/profile -curl -d '{"_links": [{"rel":"facebook", "href": "http://localhost:8080/profile/2"}]}' -H "Content-Type: application/json" http://localhost:8080/people/2/profiles \ No newline at end of file +curl -v -d '{"postalCode":"12345","province":"MO","lines":["1 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address +curl -v -d "http://localhost:8080/address/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/addresses +curl -v -d "http://localhost:8080/people/1" -X PUT -H "Content-Type: text/uri-list" http://localhost:8080/address/1/person +curl -v -d '{"postalCode":"54321","province":"MO","lines":["2 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address +curl -v -d "http://localhost:8080/address/2" -H "Content-Type: text/uri-list" http://localhost:8080/people/2/addresses +curl -v -d '{"type" : "twitter", "url": "#!/johndoe"}' -H "Content-Type: application/json" http://localhost:8080/profile +curl -v -d "http://localhost:8080/profile/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/profiles +curl -v -d '{"type" : "facebook", "url": "/janedoe"}' -H "Content-Type: application/json" http://localhost:8080/profile +curl -v -d '{"links": [{"rel":"facebook", "href": "http://localhost:8080/profile/2"}]}' -H "Content-Type: application/json" http://localhost:8080/people/2/profiles \ No newline at end of file