DATAREST-37: Display entities inline when accessing relationship links.

This commit is contained in:
Jon Brisbin
2012-08-13 09:31:55 -05:00
committed by Jon Brisbin
parent 17c97eb423
commit 3393780e65
21 changed files with 420 additions and 250 deletions

View File

@@ -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 <jbrisbin@vmware.com>
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, defaultImpl = ResourceLink.class)
public interface Link {
/**

View File

@@ -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<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

@@ -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<Link> links = new ArrayList<Link>();
/**
* 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<Link> getLinks() {
return this.links;
}
}

View File

@@ -1,22 +0,0 @@
package org.springframework.data.rest.core;
import java.util.ArrayList;
import java.util.List;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
public class Links {
private List<Link> links = new ArrayList<Link>();
public Links add(Link link) {
links.add(link);
return this;
}
public List<Link> getLinks() {
return this.links;
}
}

View File

@@ -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<T> extends LinkAware<Resource<T>> {
public class Resource<T> {
@JsonUnwrapped
protected T resource;
@JsonProperty("links")
protected Set<Link> links = new HashSet<Link>();
public Resource() {
}
@@ -20,12 +27,64 @@ public class Resource<T> extends LinkAware<Resource<T>> {
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<T> setResource(T resource) {
this.resource = resource;
return this;
}
/**
* Get the set of {@link Link}s for this resource.
*
* @return
*/
public Set<Link> getLinks() {
return links;
}
/**
* Set the entire set of {@link Link}s.
*
* @param links
*
* @return {@literal this}
*/
@SuppressWarnings({"unchecked"})
public Resource<T> setLinks(Set<Link> 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<T> addLink(Link link) {
Assert.notNull(link, "Link cannot be null.");
links.add(link);
return this;
}
}

View File

@@ -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<Link> {
@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 +
'}';
}
}

View File

@@ -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<Resource<?>> resources = new ArrayList<Resource<?>>();
@JsonProperty("links")
protected Set<Link> links = new HashSet<Link>();
/**
* Get the {@link Resource}s this {@literal ResourceSet} manages.
*
* @return
*/
public List<Resource<?>> getResources() {
return resources;
}
/**
* Set the {@link Resource}s this {@literal ResourceSet} manages.
*
* @param resources
*
* @return
*/
public ResourceSet setResources(List<Resource<?>> 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<Object>() : resource));
return this;
}
/**
* Get the set of {@link Link}s.
*
* @return
*/
public Set<Link> getLinks() {
return links;
}
/**
* Set the {@link Link}s this {@literal ResourceSet} manages.
*
* @param links
*
* @return {@literal this}
*/
@SuppressWarnings({"unchecked"})
public ResourceSet setLinks(Set<Link> 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;
}
}

View File

@@ -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<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

@@ -1,39 +0,0 @@
package org.springframework.data.rest.core;
import java.net.URI;
/**
* Implementation of {@link Link}.
*
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
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 +
'}';
}
}

View File

@@ -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 <jbrisbin@vmware.com>
*/
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);
}

View File

@@ -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;
}

View File

@@ -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<T extends AbstractReposito
*/
protected void onBeforeRenderResources(ServerHttpRequest request,
RepositoryMetadata repositoryMetadata,
Resources resources) {
ResourceSet resources) {
}
/**

View File

@@ -1,7 +1,7 @@
package org.springframework.data.rest.repository.context;
import org.springframework.data.rest.core.Resource;
import org.springframework.data.rest.core.Resources;
import org.springframework.data.rest.core.ResourceSet;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;
@@ -17,10 +17,11 @@ public abstract class RenderEvent extends RepositoryEvent {
public RenderEvent(ServerHttpRequest request, RepositoryMetadata repoMeta, Object source) {
super(source);
Assert.isTrue(source instanceof Resource || source instanceof Resources);
Assert.isTrue(source instanceof Resource || source instanceof ResourceSet,
"Event source must be of type 'Resource' or 'ResourceSet'");
this.request = request;
this.repositoryMetadata = repoMeta;
this.topLevelResource = (source instanceof Resources);
this.topLevelResource = (source instanceof ResourceSet);
}
public ServerHttpRequest getRequest() {
@@ -39,9 +40,9 @@ public abstract class RenderEvent extends RepositoryEvent {
}
}
public Resources getResources() {
if(getSource() instanceof Resources) {
return (Resources)getSource();
public ResourceSet getResources() {
if(getSource() instanceof ResourceSet) {
return (ResourceSet)getSource();
} else {
throw new IllegalStateException("Source of event is not a Resources, it's " + source);
}

View File

@@ -5,8 +5,8 @@ import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.rest.core.Resource
import org.springframework.data.rest.core.Resources
import org.springframework.data.rest.core.SimpleLink
import org.springframework.data.rest.core.ResourceSet
import org.springframework.data.rest.core.ResourceLink
import org.springframework.data.rest.repository.RepositoryExporter
import org.springframework.data.rest.repository.RepositoryMetadata
import org.springframework.data.rest.repository.annotation.HandleAfterDelete
@@ -76,8 +76,8 @@ class ExtensionsSpec extends Specification {
def request = Mock(ServerHttpRequest)
def p = new Person("John Doe")
def selfLink = new SimpleLink("self", new URI("http://localhost:8080/people/1"))
def resources = new Resources()
def selfLink = new ResourceLink("self", new URI("http://localhost:8080/people/1"))
def resources = new ResourceSet()
resources.links << selfLink
def resource = new Resource(p)
resource.links << selfLink
@@ -154,14 +154,14 @@ class PersonRenderHandler {
@HandleBeforeRenderResources void handleBeforeRenderResources(ServerHttpRequest request,
RepositoryMetadata repoMeta,
Resources resources) {
resources.links << new SimpleLink("linkAddedByHandler", new URI("http://localhost:8080/linkAddedByHandler"))
ResourceSet resources) {
resources.links << new ResourceLink("linkAddedByHandler", new URI("http://localhost:8080/linkAddedByHandler"))
}
@HandleBeforeRenderResource void handleBeforeRenderResource(ServerHttpRequest request,
RepositoryMetadata repoMeta,
Resource resource) {
resource.links << new SimpleLink("linkAddedByHandler", new URI("http://localhost:8080/linkAddedByHandler"))
resource.links << new ResourceLink("linkAddedByHandler", new URI("http://localhost:8080/linkAddedByHandler"))
}
}

View File

@@ -7,7 +7,8 @@ import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.CustomSerializerFactory;
import org.springframework.data.rest.core.SimpleLink;
import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.ResourceLink;
import org.springframework.data.rest.core.util.FluentBeanSerializer;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
@@ -28,7 +29,7 @@ public abstract class JacksonUtil {
public static MappingJacksonHttpMessageConverter createJacksonHttpMessageConverter(final ObjectMapper objectMapper) {
// We need a custom serializer for handling beans that don't conform to the JavaBeans standard 'get' and 'set'
CustomSerializerFactory customSerializerFactory = new CustomSerializerFactory();
customSerializerFactory.addSpecificMapping(SimpleLink.class, new FluentBeanSerializer(SimpleLink.class));
customSerializerFactory.addSpecificMapping(ResourceLink.class, new FluentBeanSerializer(ResourceLink.class));
objectMapper.setSerializerFactory(customSerializerFactory);
// We want to support all our custom types of JSON and also the catch-all
MappingJacksonHttpMessageConverter jsonConverter = new MappingJacksonHttpMessageConverter() {

View File

@@ -43,16 +43,16 @@ import org.springframework.data.repository.PagingAndSortingRepository;
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.LinkList;
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.ResourceLink;
import org.springframework.data.rest.core.ResourceSet;
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.PageableResourceSet;
import org.springframework.data.rest.repository.PagingMetadata;
import org.springframework.data.rest.repository.RepositoryConstraintViolationException;
import org.springframework.data.rest.repository.RepositoryExporter;
@@ -321,13 +321,13 @@ public class RepositoryRestController
UriComponentsBuilder uriBuilder) throws IOException {
URI baseUri = uriBuilder.build().toUri();
Resources resources = new Resources();
ResourceSet resources = new ResourceSet();
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);
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<String, RepositoryQueryMethod> entry : ((Map<String, RepositoryQueryMethod>)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<String, Object> resource = new HashMap<String, Object>();
for(Map.Entry<Object, Object> entry : ((Map<Object, Object>)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<Object>();
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<Link> links) {
Set<Link> 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;

View File

@@ -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;
}

View File

@@ -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<Resources> {
public interface ResponsePostProcessor extends PostProcessor<ResourceSet> {
}

View File

@@ -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<Ob
return (RepositoryMethodResponse.class.isAssignableFrom(clazz)
|| List.class.isAssignableFrom(clazz)
|| Map.class.isAssignableFrom(clazz)
|| Links.class.isAssignableFrom(clazz));
|| LinkList.class.isAssignableFrom(clazz)
|| Resource.class.isAssignableFrom(clazz));
}
@SuppressWarnings({"unchecked"})
@@ -61,17 +63,20 @@ public class UriListHttpMessageConverter extends AbstractHttpMessageConverter<Ob
throw new HttpMessageNotReadableException(e.getMessage(), e);
}
while(null != (line = reader.readLine())) {
if(links instanceof Links) {
((Links)links).add(new SimpleLink(rel, URI.create(line.trim())));
Link l = new ResourceLink(rel, URI.create(line.trim()));
if(links instanceof LinkList) {
((LinkList)links).add(l);
} else if(links instanceof List) {
((List)links).add(new SimpleLink(rel, URI.create(line.trim())));
((List)links).add(l);
} else if(links instanceof Map) {
List l = (List)((Map)links).get("_links");
if(null == l) {
l = new ArrayList();
((Map)links).put("_links", l);
List linksFromMap = (List)((Map)links).get("links");
if(null == linksFromMap) {
linksFromMap = new ArrayList();
((Map)links).put("links", linksFromMap);
}
l.add(new SimpleLink(rel, URI.create(line.trim())));
linksFromMap.add(l);
} else if(links instanceof Resource) {
((Resource)links).addLink(l);
}
}
return links;
@@ -82,8 +87,8 @@ public class UriListHttpMessageConverter extends AbstractHttpMessageConverter<Ob
throws IOException,
HttpMessageNotWritableException {
OutputStream body = outputMessage.getBody();
if(links instanceof Links) {
for(Link link : ((Links)links).getLinks()) {
if(links instanceof LinkList) {
for(Link link : ((LinkList)links).getLinks()) {
body.write(link.href().toASCIIString().getBytes());
body.write('\n');
}
@@ -97,9 +102,11 @@ public class UriListHttpMessageConverter extends AbstractHttpMessageConverter<Ob
body.write('\n');
}
} else if(links instanceof Map) {
writeInternal(((Map)links).get("_links"), outputMessage);
writeInternal(((Map)links).get("links"), outputMessage);
} else if(links instanceof RepositoryMethodResponse) {
writeInternal(((RepositoryMethodResponse)links).getLinks(), outputMessage);
} else if(links instanceof Resource) {
writeInternal(((Resource)links).getLinks(), outputMessage);
}
}

View File

@@ -4,7 +4,7 @@ import org.codehaus.jackson.map.ObjectMapper
import org.codehaus.jackson.map.ser.CustomSerializerFactory
import org.springframework.context.support.ClassPathXmlApplicationContext
import org.springframework.data.domain.PageRequest
import org.springframework.data.rest.core.SimpleLink
import org.springframework.data.rest.core.util.FluentBeanSerializer
import org.springframework.data.rest.test.webmvc.Address
import org.springframework.data.rest.webmvc.PagingAndSorting
@@ -26,6 +26,7 @@ import spock.lang.Shared
import spock.lang.Specification
import javax.persistence.EntityManagerFactory
import org.springframework.data.rest.core.ResourceLink
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
@@ -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)
}

View File

@@ -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
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