Integrated Spring HATEOAS.

This commit is contained in:
Jon Brisbin
2012-08-27 15:16:02 -05:00
committed by Jon Brisbin
parent d95eda8d9c
commit 40645673bf
41 changed files with 457 additions and 1150 deletions

View File

@@ -80,7 +80,7 @@ configure(subprojects) { subproject ->
// Testing
testCompile "org.spockframework:spock-core:$spockVersion"
testCompile "org.spockframework:spock-spring:$spockVersion"
testCompile "org.hamcrest:hamcrest-library:1.2.1"
testCompile "org.hamcrest:hamcrest-library:1.3"
testCompile "org.springframework:spring-test:$springVersion"
testRuntime "org.springframework:spring-context-support:$springVersion"
testCompile "org.mockito:mockito-core:1.8.5"
@@ -96,6 +96,8 @@ idea {
downloadSources = true
}
project {
jdkName = "OpenJDK 1.7"
languageLevel = "1.6"
ipr {
withXml { xml ->
xml.node.component.find { it.@name == "VcsDirectoryMappings" }.mapping.@vcs = "Git"

View File

@@ -12,6 +12,7 @@ groovyVersion = 1.8.6
# Supporting libraries
sdCommonsVersion = 1.4.0.BUILD-SNAPSHOT
sdJpaVersion = 1.2.0.BUILD-SNAPSHOT
hateoasVersion = 0.3.0.BUILD-SNAPSHOT
jacksonVersion = 1.9.7
hibernateVersion = 4.1.4.Final

View File

@@ -1,28 +0,0 @@
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>
*/
public interface Link {
/**
* The text used in the {@literal rel} attribute.
*
* @return {@literal rel} attribute text.
*/
String rel();
/**
* The {@link URI} this link is referencing.
*
* @return {@link URI} of this link. Should not be null.
*/
URI href();
}

View File

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

View File

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

View File

@@ -1,90 +0,0 @@
package org.springframework.data.rest.core;
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> {
@JsonUnwrapped
protected T resource;
@JsonProperty("links")
protected Set<Link> links = new HashSet<Link>();
public Resource() {
}
public Resource(T resource) {
this.resource = resource;
}
/**
* Get the resource. May be {@literal null}.
*
* @return The resource or {@literal null}.
*/
public T getResource() {
return 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

@@ -1,79 +0,0 @@
package org.springframework.data.rest.core;
import java.net.URI;
import org.codehaus.jackson.annotate.JsonProperty;
import org.springframework.util.Assert;
/**
* 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;
}
public String getRel() {
return rel();
}
public ResourceLink setRel(String rel) {
this.rel = rel;
return this;
}
public URI getHref() {
return href();
}
public ResourceLink setHref(URI href) {
Assert.notNull(href, "href URI cannot be null.");
this.href = href;
return this;
}
@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

@@ -1,101 +0,0 @@
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

@@ -7,7 +7,7 @@ import java.net.URI;
*
* @author Jon Brisbin
*/
public interface Resolver<T> {
public interface UriResolver<T> {
/**
* Take a {@link URI} and resolve it to an actual object.

View File

@@ -193,4 +193,16 @@ public abstract class UriUtils {
return uris.size() > 0 ? uris.get(Math.max(uris.size() - 1, 0)) : null;
}
/**
* Create a new {@link URI} out of the components.
*
* @param baseUri
* @param pathSegments
*
* @return
*/
public static URI buildUri(URI baseUri, String... pathSegments) {
return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri();
}
}

View File

@@ -11,6 +11,9 @@ dependencies {
//compile "org.springframework.data:spring-data-commons-core:$sdCommonsVersion"
compile "org.springframework.data:spring-data-jpa:$sdJpaVersion"
// Spring HATEOAS
compile "org.springframework.hateoas:spring-hateoas:$hateoasVersion"
// Exporter core
compile project(":spring-data-rest-core")

View File

@@ -1,23 +0,0 @@
package org.springframework.data.rest.repository;
import org.codehaus.jackson.annotate.JsonProperty;
import org.springframework.data.rest.core.ResourceSet;
/**
* @author Jon Brisbin
*/
public class PageableResourceSet extends ResourceSet {
@JsonProperty("page")
protected PagingMetadata paging = new PagingMetadata(-1, 20, 0, 0);
public PagingMetadata getPaging() {
return paging;
}
public PageableResourceSet setPaging(PagingMetadata paging) {
this.paging = paging;
return this;
}
}

View File

@@ -10,6 +10,9 @@ public class PagingMetadata {
private int totalPages = 0;
private long totalElements = 0;
public PagingMetadata() {
}
public PagingMetadata(int number,
int size,
int totalPages,

View File

@@ -9,7 +9,7 @@ import java.util.Stack;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.Resolver;
import org.springframework.data.rest.core.UriResolver;
import org.springframework.data.rest.core.util.UriUtils;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.ClassUtils;
@@ -17,9 +17,9 @@ import org.springframework.util.ClassUtils;
/**
* @author Jon Brisbin
*/
public class UriToDomainObjectResolver
extends RepositoryExporterSupport<UriToDomainObjectResolver>
implements Resolver<Object> {
public class UriToDomainObjectUriResolver
extends RepositoryExporterSupport<UriToDomainObjectUriResolver>
implements UriResolver<Object> {
@Autowired(required = false)
private List<ConversionService> conversionServices = Arrays.<ConversionService>asList(new DefaultFormattingConversionService());
@@ -28,7 +28,7 @@ public class UriToDomainObjectResolver
return conversionServices;
}
public UriToDomainObjectResolver setConversionServices(List<ConversionService> conversionServices) {
public UriToDomainObjectUriResolver setConversionServices(List<ConversionService> conversionServices) {
this.conversionServices = conversionServices;
return this;
}

View File

@@ -7,12 +7,8 @@ import org.springframework.beans.factory.annotation.Autowired;
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.ResourceSet;
import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.http.server.ServerHttpRequest;
/**
* Abstract class that listens for generic {@link RepositoryEvent}s and dispatches them to a specific
@@ -54,13 +50,6 @@ public abstract class AbstractRepositoryEventListener<T extends AbstractReposito
onBeforeDelete(event.getSource());
} else if(event instanceof AfterDeleteEvent) {
onAfterDelete(event.getSource());
} else if(event instanceof RenderEvent) {
RenderEvent ev = (RenderEvent)event;
if(ev.isTopLevelResource()) {
onBeforeRenderResources(ev.getRequest(), ev.getRepositoryMetadata(), ev.getResources());
} else {
onBeforeRenderResource(ev.getRequest(), ev.getRepositoryMetadata(), ev.getResource());
}
}
}
@@ -132,32 +121,4 @@ public abstract class AbstractRepositoryEventListener<T extends AbstractReposito
protected void onAfterDelete(Object entity) {
}
/**
* Override this method if you are interested in {@literal beforeRender} for top-level events. These are events
* triggered by the exporter before sending out a wrapped, top-level response for queries, entity lists, and results
* that are pagable.
*
* @param request
* @param repositoryMetadata
* @param resources
*/
protected void onBeforeRenderResources(ServerHttpRequest request,
RepositoryMetadata repositoryMetadata,
ResourceSet resources) {
}
/**
* Override this method if you are interested in {@literal beforeRender} for entity events. These are events emitted
* by the exporter before sending out an entity representation to the client. These events are triggered when
* requesting individual entities and specific properties of an entity.
*
* @param request
* @param repositoryMetadata
* @param resource
*/
protected void onBeforeRenderResource(ServerHttpRequest request,
RepositoryMetadata repositoryMetadata,
Resource resource) {
}
}

View File

@@ -21,8 +21,6 @@ import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave;
import org.springframework.data.rest.repository.annotation.HandleAfterSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete;
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave;
import org.springframework.data.rest.repository.annotation.HandleBeforeRenderResource;
import org.springframework.data.rest.repository.annotation.HandleBeforeRenderResources;
import org.springframework.data.rest.repository.annotation.HandleBeforeSave;
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler;
import org.springframework.util.ClassUtils;
@@ -122,16 +120,6 @@ public class AnnotatedHandlerRepositoryEventListener implements ApplicationListe
inspect(targetType, handler, method, HandleAfterLinkSave.class, AfterLinkSaveEvent.class);
inspect(targetType, handler, method, HandleBeforeDelete.class, BeforeDeleteEvent.class);
inspect(targetType, handler, method, HandleAfterDelete.class, AfterDeleteEvent.class);
inspect(targetType,
handler,
method,
HandleBeforeRenderResource.class,
BeforeRenderResourceEvent.class);
inspect(targetType,
handler,
method,
HandleBeforeRenderResources.class,
BeforeRenderResourcesEvent.class);
}
},
new ReflectionUtils.MethodFilter() {
@@ -158,13 +146,7 @@ public class AnnotatedHandlerRepositoryEventListener implements ApplicationListe
try {
Object src = event.getSource();
if(event instanceof RenderEvent) {
RenderEvent ev = (RenderEvent)event;
if(!ClassUtils.isAssignable(handlerMethod.targetType,
ev.getRepositoryMetadata().entityMetadata().type())) {
continue;
}
} else if(!ClassUtils.isAssignable(handlerMethod.targetType, src.getClass())) {
if(!ClassUtils.isAssignable(handlerMethod.targetType, src.getClass())) {
continue;
}
@@ -174,10 +156,6 @@ public class AnnotatedHandlerRepositoryEventListener implements ApplicationListe
params.add(((BeforeLinkSaveEvent)event).getLinked());
} else if(event instanceof AfterLinkSaveEvent) {
params.add(((AfterLinkSaveEvent)event).getLinked());
} else if(event instanceof RenderEvent) {
RenderEvent ev = (RenderEvent)event;
params.add(0, ev.getRequest());
params.add(1, ev.getRepositoryMetadata());
}
handlerMethod.method.invoke(handlerMethod.handler, params.toArray());

View File

@@ -1,13 +0,0 @@
package org.springframework.data.rest.repository.context;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.http.server.ServerHttpRequest;
/**
* @author Jon Brisbin
*/
public class BeforeRenderResourceEvent extends RenderEvent {
public BeforeRenderResourceEvent(ServerHttpRequest request, RepositoryMetadata repoMeta, Object source) {
super(request, repoMeta, source);
}
}

View File

@@ -1,17 +0,0 @@
package org.springframework.data.rest.repository.context;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.http.server.ServerHttpRequest;
/**
* Event emitted before the the object is rendered to the client. Implementations of {@link
* AbstractRepositoryEventListener} can listen for these events and alter the output of the resource being sent to the
* link.
*
* @author Jon Brisbin
*/
public class BeforeRenderResourcesEvent extends RenderEvent {
public BeforeRenderResourcesEvent(ServerHttpRequest request, RepositoryMetadata repoMeta, Object source) {
super(request, repoMeta, source);
}
}

View File

@@ -1,55 +0,0 @@
package org.springframework.data.rest.repository.context;
import org.springframework.data.rest.core.Resource;
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;
/**
* @author Jon Brisbin
*/
public abstract class RenderEvent extends RepositoryEvent {
protected final ServerHttpRequest request;
protected final RepositoryMetadata repositoryMetadata;
protected final boolean topLevelResource;
public RenderEvent(ServerHttpRequest request, RepositoryMetadata repoMeta, Object source) {
super(source);
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 ResourceSet);
}
public ServerHttpRequest getRequest() {
return request;
}
public RepositoryMetadata getRepositoryMetadata() {
return repositoryMetadata;
}
public Resource getResource() {
if(getSource() instanceof Resource) {
return (Resource)getSource();
} else {
throw new IllegalStateException("Source of event is not a Resource, it's " + source);
}
}
public ResourceSet getResources() {
if(getSource() instanceof ResourceSet) {
return (ResourceSet)getSource();
} else {
throw new IllegalStateException("Source of event is not a Resources, it's " + source);
}
}
public boolean isTopLevelResource() {
return topLevelResource;
}
}

View File

@@ -6,7 +6,7 @@ import java.util.Iterator;
import java.util.List;
import org.codehaus.jackson.annotate.JsonProperty;
import org.springframework.data.rest.core.Link;
import org.springframework.hateoas.Link;
/**
* JSON-serializable response for returns that have a mix of results and links. Also used in responses that have just

View File

@@ -4,18 +4,12 @@ import org.springframework.beans.factory.annotation.Autowired
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.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
import org.springframework.data.rest.repository.annotation.HandleAfterLinkSave
import org.springframework.data.rest.repository.annotation.HandleAfterSave
import org.springframework.data.rest.repository.annotation.HandleBeforeDelete
import org.springframework.data.rest.repository.annotation.HandleBeforeLinkSave
import org.springframework.data.rest.repository.annotation.HandleBeforeRenderResource
import org.springframework.data.rest.repository.annotation.HandleBeforeRenderResources
import org.springframework.data.rest.repository.annotation.HandleBeforeSave
import org.springframework.data.rest.repository.annotation.RepositoryEventHandler
import org.springframework.data.rest.repository.context.AfterDeleteEvent
@@ -24,12 +18,9 @@ import org.springframework.data.rest.repository.context.AfterSaveEvent
import org.springframework.data.rest.repository.context.AnnotatedHandlerRepositoryEventListener
import org.springframework.data.rest.repository.context.BeforeDeleteEvent
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent
import org.springframework.data.rest.repository.context.BeforeRenderResourceEvent
import org.springframework.data.rest.repository.context.BeforeRenderResourcesEvent
import org.springframework.data.rest.repository.context.BeforeSaveEvent
import org.springframework.data.rest.repository.test.ApplicationConfig
import org.springframework.data.rest.repository.test.Person
import org.springframework.http.server.ServerHttpRequest
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
@@ -69,31 +60,6 @@ class ExtensionsSpec extends Specification {
}
def "responds to render events"() {
given:
def repoMeta = exporter.repositoryMetadataFor(Person)
def request = Mock(ServerHttpRequest)
def p = new Person("John Doe")
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
when:
appCtx.publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources))
appCtx.publishEvent(new BeforeRenderResourceEvent(request, repoMeta, resource))
then:
resources.links.size() == 2
null != resources.links.find { it.rel() == "linkAddedByHandler" }
resource.links.size() == 2
null != resources.links.find { it.rel() == "linkAddedByHandler" }
}
}
@Configuration
@@ -107,10 +73,6 @@ class EventsApplicationConfig {
new PersonEventHandler()
}
@Bean PersonRenderHandler personRenderHandler() {
new PersonRenderHandler()
}
}
@RepositoryEventHandler(Person)
@@ -149,19 +111,3 @@ class PersonEventHandler {
}
@RepositoryEventHandler(Person)
class PersonRenderHandler {
@HandleBeforeRenderResources void handleBeforeRenderResources(ServerHttpRequest request,
RepositoryMetadata repoMeta,
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 ResourceLink("linkAddedByHandler", new URI("http://localhost:8080/linkAddedByHandler"))
}
}

View File

@@ -15,8 +15,7 @@ dependencies {
// Repository Exporter support
compile project(":spring-data-rest-repository")
compile "org.springframework.hateoas:spring-hateoas:0.3.0.BUILD-SNAPSHOT"
compile "org.springframework.data:spring-data-commons-core:$sdCommonsVersion"
runtime "org.hibernate:hibernate-entitymanager:$hibernateVersion"

View File

@@ -0,0 +1,55 @@
package org.springframework.data.rest.webmvc;
import static org.springframework.data.rest.core.util.UriUtils.*;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.codehaus.jackson.annotate.JsonAnyGetter;
import org.springframework.data.rest.repository.AttributeMetadata;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
/**
* @author Jon Brisbin
*/
public class EntityResource extends Resource<Map<String, Object>> {
public EntityResource(Map<String, Object> dto, Set<Link> links) {
super(dto, links);
}
@SuppressWarnings({"unchecked"})
public static EntityResource wrap(Object entity, RepositoryMetadata repoMeta, URI baseUri) {
Set<Link> links = new HashSet<Link>();
for(Object attrName : repoMeta.entityMetadata().linkedAttributes().keySet()) {
URI uri = buildUri(baseUri, attrName.toString());
String rel = repoMeta.rel() + "." + entity.getClass().getSimpleName() + "." + attrName;
links.add(new Link(uri.toString(), rel));
}
links.add(new Link(baseUri.toString(), "self"));
Map<String, Object> entityDto = new HashMap<String, Object>();
for(Map.Entry<String, AttributeMetadata> attrMeta : ((Map<String, AttributeMetadata>)repoMeta.entityMetadata()
.embeddedAttributes()).entrySet()) {
String name = attrMeta.getKey();
Object val;
if(null != (val = attrMeta.getValue().get(entity))) {
entityDto.put(name, val);
}
}
return new EntityResource(entityDto, links);
}
@JsonAnyGetter
@Override public Map<String, Object> getContent() {
return super.getContent();
}
}

View File

@@ -26,7 +26,6 @@ import org.codehaus.jackson.map.Module;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.deser.std.StdDeserializer;
import org.codehaus.jackson.map.module.SimpleAbstractTypeResolver;
import org.codehaus.jackson.map.module.SimpleDeserializers;
import org.codehaus.jackson.map.module.SimpleKeyDeserializers;
import org.codehaus.jackson.map.module.SimpleModule;
@@ -34,14 +33,15 @@ import org.codehaus.jackson.map.module.SimpleSerializers;
import org.codehaus.jackson.map.ser.std.SerializerBase;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.ResourceLink;
import org.springframework.data.rest.repository.AttributeMetadata;
import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.UriToDomainObjectResolver;
import org.springframework.data.rest.repository.UriToDomainObjectUriResolver;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
@@ -55,18 +55,19 @@ import org.springframework.web.util.UriComponentsBuilder;
*/
public class RepositoryAwareMappingHttpMessageConverter
extends MappingJacksonHttpMessageConverter
implements InitializingBean {
private final ObjectMapper mapper = new ObjectMapper();
implements ApplicationEventPublisherAware,
InitializingBean {
private final ObjectMapper mapper = new ObjectMapper();
@Autowired(required = false)
protected List<ConversionService> conversionServices = Arrays.<ConversionService>asList(new DefaultFormattingConversionService());
protected List<ConversionService> conversionServices = Arrays.<ConversionService>asList(new DefaultFormattingConversionService());
@Autowired(required = false)
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
@Autowired(required = false)
protected List<Module> modules = Collections.emptyList();
protected List<Module> modules = Collections.emptyList();
@Autowired
protected UriToDomainObjectResolver domainObjectResolver;
protected UriToDomainObjectUriResolver domainObjectResolver = null;
protected ApplicationEventPublisher eventPublisher = null;
public RepositoryAwareMappingHttpMessageConverter() {
setSupportedMediaTypes(Arrays.asList(
@@ -77,6 +78,10 @@ public class RepositoryAwareMappingHttpMessageConverter
setObjectMapper(mapper);
}
@Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
}
@Override public void afterPropertiesSet() throws Exception {
mapper.registerModule(new RepositoryAwareModule());
for(Module m : modules) {
@@ -114,11 +119,11 @@ public class RepositoryAwareMappingHttpMessageConverter
return this;
}
public UriToDomainObjectResolver getDomainObjectResolver() {
public UriToDomainObjectUriResolver getDomainObjectResolver() {
return domainObjectResolver;
}
public RepositoryAwareMappingHttpMessageConverter setDomainObjectResolver(UriToDomainObjectResolver domainObjectResolver) {
public RepositoryAwareMappingHttpMessageConverter setDomainObjectResolver(UriToDomainObjectUriResolver domainObjectResolver) {
this.domainObjectResolver = domainObjectResolver;
return this;
}
@@ -199,16 +204,11 @@ public class RepositoryAwareMappingHttpMessageConverter
SimpleKeyDeserializers keyDsers = new SimpleKeyDeserializers();
private RepositoryAwareModule() {
super("RepositoryAwareModule", new Version(1, 0, 0, "SNAPSHOT"));
super("RepositoryAwareModule", Version.unknownVersion());
}
@SuppressWarnings({"unchecked"})
@Override public void setupModule(SetupContext context) {
context.addAbstractTypeResolver(
new SimpleAbstractTypeResolver()
.addMapping(Link.class, ResourceLink.class)
);
for(RepositoryExporter repoExp : repositoryExporters) {
for(String repoName : new ArrayList<String>(repoExp.repositoryNames())) {
RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName);
@@ -272,7 +272,7 @@ public class RepositoryAwareMappingHttpMessageConverter
String rel = repoMeta.rel() + "." + repoMeta.domainType().getSimpleName();
URI selfUri = buildUri(RepositoryRestController.BASE_URI.get(), repoMeta.name(), sId);
jgen.writeObject(new ResourceLink(rel, selfUri));
jgen.writeObject(new Link(selfUri.toString(), rel));
}
}

View File

@@ -1,13 +1,9 @@
package org.springframework.data.rest.webmvc;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
@@ -22,18 +18,16 @@ public class RepositoryRestConfiguration {
public static final RepositoryRestConfiguration DEFAULT = new RepositoryRestConfiguration();
private int defaultPageSize = 20;
private String pageParamName = "page";
private String limitParamName = "limit";
private String sortParamName = "sort";
private String jsonpParamName = "callback";
private String jsonpOnErrParamName = null;
private List<HttpMessageConverter<?>> customConverters = Collections.emptyList();
private Multimap<Class<?>, ResourcePostProcessor> resourcePostProcessors = ArrayListMultimap.create();
private List<ResourceSetPostProcessor> resourceSetPostProcessors = Collections.emptyList();
private Map<Class<?>, Class<?>> typeMappings = Collections.emptyMap();
private MediaType defaultMediaType = MediaType.APPLICATION_JSON;
private boolean dumpErrors = true;
private int defaultPageSize = 20;
private String pageParamName = "page";
private String limitParamName = "limit";
private String sortParamName = "sort";
private String jsonpParamName = "callback";
private String jsonpOnErrParamName = null;
private List<HttpMessageConverter<?>> customConverters = Collections.emptyList();
private Map<Class<?>, Class<?>> typeMappings = Collections.emptyMap();
private MediaType defaultMediaType = MediaType.APPLICATION_JSON;
private boolean dumpErrors = true;
/**
* Get the default size of {@link org.springframework.data.domain.Pageable}s. Default is 20.
@@ -257,74 +251,4 @@ public class RepositoryRestConfiguration {
return this;
}
/**
* Get the list of {@link ResourceSetPostProcessor}s that will potentially alter the responses going back to the
* client.
*
* @return
*/
public List<ResourceSetPostProcessor> getResourceSetPostProcessors() {
return resourceSetPostProcessors;
}
/**
* Set the list of {@link ResourceSetPostProcessor}s that will potentially alter the responses going back to the
*
* @param resourceSetPostProcessors
*/
@Autowired(required = false)
public RepositoryRestConfiguration setResourceSetPostProcessors(List<ResourceSetPostProcessor> resourceSetPostProcessors) {
Assert.notNull(resourceSetPostProcessors, "ResourceSetPostProcessors cannot be null.");
this.resourceSetPostProcessors = resourceSetPostProcessors;
return this;
}
/**
* 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) {
Assert.notNull(type, "Type for ResourcePostProcessor cannot be null.");
Assert.notNull(postProcessor, "ResourcePostProcessor for type " + type.getName() + " cannot be null.");
resourcePostProcessors.put(type, postProcessor);
return this;
}
/**
* Set tje {@link ResourcePostProcessor} map used to determine what post-processors to run for which domain type.
*
* @param postProcessors
*
* @return
*/
public RepositoryRestConfiguration setResourcePostProcessors(Map<Class<?>, ResourcePostProcessor> postProcessors) {
if(null == postProcessors) {
return this;
}
for(Map.Entry<Class<?>, ResourcePostProcessor> entry : postProcessors.entrySet()) {
addResourcePostProcessor(entry.getKey(), entry.getValue());
}
return this;
}
/**
* Get the {@link ResourcePostProcessor}s assigned to a particular domain type.
*
* @param type
*
* @return
*/
public Collection<ResourcePostProcessor> getResourcePostProcessors(Class<?> type) {
Collection<ResourcePostProcessor> pps = resourcePostProcessors.get(type);
if(null == pps) {
return Collections.emptyList();
} else {
return pps;
}
}
}

View File

@@ -1,5 +1,7 @@
package org.springframework.data.rest.webmvc;
import static org.springframework.data.rest.core.util.UriUtils.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -45,23 +47,14 @@ import org.springframework.data.repository.CrudRepository;
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.LinkList;
import org.springframework.data.rest.core.MapResource;
import org.springframework.data.rest.core.Resource;
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.repository.AttributeMetadata;
import org.springframework.data.rest.repository.EntityMetadata;
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;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.RepositoryNotFoundException;
import org.springframework.data.rest.repository.UriToDomainObjectResolver;
import org.springframework.data.rest.repository.UriToDomainObjectUriResolver;
import org.springframework.data.rest.repository.annotation.RestResource;
import org.springframework.data.rest.repository.context.AfterDeleteEvent;
import org.springframework.data.rest.repository.context.AfterLinkDeleteEvent;
@@ -70,14 +63,16 @@ import org.springframework.data.rest.repository.context.AfterSaveEvent;
import org.springframework.data.rest.repository.context.BeforeDeleteEvent;
import org.springframework.data.rest.repository.context.BeforeLinkDeleteEvent;
import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent;
import org.springframework.data.rest.repository.context.BeforeRenderResourceEvent;
import org.springframework.data.rest.repository.context.BeforeRenderResourcesEvent;
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.MethodParameterConversionService;
import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
@@ -138,7 +133,8 @@ public class RepositoryRestController
public static final String LOCATION = "Location";
public static final String SELF = "self";
final static ThreadLocal<URI> BASE_URI = new ThreadLocal<URI>();
private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class);
private static final Logger LOG = LoggerFactory.getLogger(
RepositoryRestController.class);
private static final TypeDescriptor STRING_ARRAY_TYPE = TypeDescriptor.valueOf(String[].class);
/**
@@ -163,7 +159,7 @@ public class RepositoryRestController
private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT;
private ObjectMapper objectMapper = new ObjectMapper();
private RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter;
private UriToDomainObjectResolver domainObjectResolver;
private UriToDomainObjectUriResolver domainObjectResolver;
private ApplicationContext applicationContext;
{
@@ -313,12 +309,12 @@ public class RepositoryRestController
return this;
}
public UriToDomainObjectResolver getDomainObjectResolver() {
public UriToDomainObjectUriResolver getDomainObjectResolver() {
return domainObjectResolver;
}
@Autowired
public RepositoryRestController setDomainObjectResolver(UriToDomainObjectResolver domainObjectResolver) {
public RepositoryRestController setDomainObjectResolver(UriToDomainObjectUriResolver domainObjectResolver) {
this.domainObjectResolver = domainObjectResolver;
return this;
}
@@ -353,24 +349,20 @@ public class RepositoryRestController
URI baseUri = uriBuilder.build().toUri();
BASE_URI.set(baseUri);
ResourceSet resources = new ResourceSet();
List<Link> links = new ArrayList<Link>();
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 ResourceLink(rel, path));
links.add(new Link(path.toString(), rel));
}
}
// Publish an event that we're about to publish this ResourceSet
publishEvent(new BeforeRenderResourcesEvent(request, null, resources));
// Run any configured post processors
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
new Resources<String>(Collections.<String>emptyList(), links));
}
/**
@@ -405,21 +397,23 @@ public class RepositoryRestController
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Iterator allEntities = Collections.emptyList().iterator();
ResourceSet resources;
if(repoMeta.repository() instanceof PagingAndSortingRepository) {
PageableResourceSet pr = new PageableResourceSet();
Set<Link> links = new HashSet<Link>();
PagedResources.PageMetadata pageMeta = null;
Iterator allEntities = Collections.emptyList().iterator();
if(repoMeta.repository() instanceof PagingAndSortingRepository) {
Page page = ((PagingAndSortingRepository)repoMeta.repository()).findAll(pageSort);
if(page.hasContent()) {
allEntities = page.iterator();
}
// Set page counts in the response
pr.setPaging(new PagingMetadata(page.getNumber() + 1,
page.getSize(),
page.getTotalPages(),
page.getTotalElements()));
pageMeta = new PagedResources.PageMetadata(
page.getSize(),
page.getNumber() + 1,
page.getTotalElements(),
page.getTotalPages()
);
// Copy over parameters
UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository);
@@ -439,7 +433,7 @@ public class RepositoryRestController
!page.isFirstPage() && page.hasPreviousPage(),
page.getNumber(),
"prev",
pr.getLinks()
links
);
maybeAddPrevNextLink(
nextPrevBase,
@@ -449,48 +443,39 @@ public class RepositoryRestController
!page.isLastPage() && page.hasNextPage(),
page.getNumber() + 2,
"next",
pr.getLinks()
links
);
resources = pr;
} else {
Iterable it = repoMeta.repository().findAll();
if(null != it) {
allEntities = it.iterator();
}
resources = new ResourceSet();
}
List<Resource<Map<String, Object>>> allResources = new ArrayList<Resource<Map<String, Object>>>();
while(allEntities.hasNext()) {
Object o = allEntities.next();
Serializable id = (Serializable)repoMeta.entityMetadata().idAttribute().get(o);
if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) {
resources.addLink(new ResourceLink(repoMeta.rel() + "." + o.getClass().getSimpleName(),
buildUri(baseUri, repository, id.toString())));
links.add(new Link(buildUri(baseUri, repository, id.toString()).toString(),
repoMeta.rel() + "." + o.getClass().getSimpleName()));
} else {
URI selfUri = buildUri(baseUri, repository, id.toString());
MapResource res = createResource(repoMeta.rel(),
o,
repoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(SELF, selfUri));
resources.addResource(res);
allResources.add(EntityResource.wrap(o, repoMeta, selfUri));
}
}
if(!repoMeta.queryMethods().isEmpty()) {
resources.addLink(new ResourceLink(repoMeta.rel() + ".search",
buildUri(baseUri, repository, "search")));
links.add(new Link(buildUri(baseUri, repository, "search").toString(),
repoMeta.rel() + ".search"));
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
// Run any configured post processors
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
(null != pageMeta
? new PagedResources(allResources, pageMeta, links)
: new Resources(allResources, links)));
}
/**
@@ -517,7 +502,7 @@ public class RepositoryRestController
BASE_URI.set(baseUri);
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
ResourceSet resources = new ResourceSet();
Set<Link> links = new HashSet<Link>();
for(Map.Entry<String, RepositoryQueryMethod> entry : ((Map<String, RepositoryQueryMethod>)repoMeta.queryMethods())
.entrySet()) {
@@ -527,29 +512,26 @@ public class RepositoryRestController
Method m = entry.getValue().method();
if(m.isAnnotationPresent(RestResource.class)) {
RestResource resourceAnno = m.getAnnotation(RestResource.class);
resources.addLink(new ResourceLink(
(StringUtils.hasText(resourceAnno.rel())
? repoMeta.rel() + "." + resourceAnno.rel()
: repoMeta.rel() + "." + entry.getKey()),
links.add(new Link(
buildUri(baseSearchUri,
(StringUtils.hasText(resourceAnno.path())
? resourceAnno.path()
: entry.getKey()))
: entry.getKey())).toString(),
(StringUtils.hasText(resourceAnno.rel())
? repoMeta.rel() + "." + resourceAnno.rel()
: repoMeta.rel() + "." + entry.getKey())
));
} else {
// No customizations, use the default
resources.addLink(new ResourceLink(repoMeta.rel() + "." + entry.getKey(),
buildUri(baseSearchUri, entry.getKey())));
links.add(new Link(buildUri(baseSearchUri, entry.getKey()).toString(),
repoMeta.rel() + "." + entry.getKey()));
}
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
// Run any configured post processors
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
new Resources<String>(Collections.<String>emptyList(), links));
}
/**
@@ -643,10 +625,15 @@ public class RepositoryRestController
Object result;
if(null == (result = queryMethod.method().invoke(repo, paramVals))) {
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), new ResourceSet());
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
new Resources<String>(Collections.<String>emptyList()));
}
ResourceSet resources = new ResourceSet();
Set<org.springframework.hateoas.Link> links = new HashSet<org.springframework.hateoas.Link>();
PagedResources.PageMetadata pageMetadata = null;
Iterator entities = Collections.emptyList().iterator();
if(result instanceof Collection) {
entities = ((Collection)result).iterator();
@@ -658,11 +645,10 @@ public class RepositoryRestController
}
// Set page counts in the response
PageableResourceSet pr = new PageableResourceSet();
pr.setPaging(new PagingMetadata(page.getNumber() + 1,
page.getSize(),
page.getTotalPages(),
page.getTotalElements()));
pageMetadata = new PagedResources.PageMetadata(page.getSize(),
page.getNumber() + 1,
page.getTotalElements(),
page.getTotalPages());
// Copy over parameters
UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository, "search", query);
@@ -682,7 +668,7 @@ public class RepositoryRestController
!page.isFirstPage() && page.hasPreviousPage(),
page.getNumber(),
"prev",
pr.getLinks()
links
);
maybeAddPrevNextLink(
nextPrevBase,
@@ -692,48 +678,41 @@ public class RepositoryRestController
!page.isLastPage() && page.hasNextPage(),
page.getNumber() + 2,
"next",
pr.getLinks()
links
);
resources = pr;
} else {
entities = Collections.singletonList(result).iterator();
}
List<Object> results = new ArrayList<Object>();
while(entities.hasNext()) {
Object obj = entities.next();
RepositoryMetadata elemRepoMeta;
if(null == (elemRepoMeta = repositoryMetadataFor(obj.getClass()))) {
resources.addResource(new Resource(obj));
if(!hasRepositoryMetadataFor(obj.getClass())) {
results.add(obj);
continue;
}
// This object is managed by a repository
RepositoryMetadata elemRepoMeta = repositoryMetadataFor(obj.getClass());
String id = elemRepoMeta.entityMetadata().idAttribute().get(obj).toString();
if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) {
String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName();
URI path = buildUri(baseUri, repository, id);
resources.addLink(new ResourceLink(rel, path));
links.add(new org.springframework.hateoas.Link(path.toString(), rel));
} else {
URI selfUri = buildUri(baseUri, repository, id);
MapResource res = createResource(repoMeta.rel(),
obj,
repoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(SELF, selfUri));
resources.addResource(res);
results.add(EntityResource.wrap(obj, repoMeta, selfUri));
}
}
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, resources));
// Run any configured post processors
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), resources);
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
(null != pageMetadata
? PagedResources.wrap(results, pageMetadata)
: Resources.wrap(results)));
}
/**
@@ -788,19 +767,7 @@ public class RepositoryRestController
Resource<?> body = null;
if(returnBody(request)) {
MapResource resource = createResource(repoMeta.rel(),
savedEntity,
repoMeta.entityMetadata(),
selfUri);
resource.addLink(new ResourceLink(SELF, selfUri));
body = resource;
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, body));
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(repoMeta.domainType())) {
body = pp.postProcess(request, body);
}
body = EntityResource.wrap(savedEntity, repoMeta, selfUri);
}
return negotiateResponse(request, HttpStatus.CREATED, headers, body);
@@ -860,19 +827,10 @@ public class RepositoryRestController
}
URI selfUri = buildUri(baseUri, repository, id);
Resource res = createResource(repoMeta.rel(),
entity,
repoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(SELF, selfUri));
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, res));
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(repoMeta.domainType())) {
res = pp.postProcess(request, res);
}
return negotiateResponse(request, HttpStatus.OK, headers, res);
return negotiateResponse(request,
HttpStatus.OK,
headers,
EntityResource.wrap(entity, repoMeta, selfUri));
}
/**
@@ -951,19 +909,7 @@ public class RepositoryRestController
Object body = null;
if(returnBody(request)) {
Resource res = createResource(repoMeta.rel(),
savedEntity,
repoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(SELF, selfUri));
body = res;
publishEvent(new BeforeRenderResourceEvent(request, repoMeta, body));
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(repoMeta.domainType())) {
res = pp.postProcess(request, res);
}
body = EntityResource.wrap(savedEntity, repoMeta, selfUri);
}
if(!isUpdate) {
@@ -1087,39 +1033,37 @@ public class RepositoryRestController
return notFoundResponse(request);
}
Object body;
Set<Link> links = new HashSet<Link>();
AttributeMetadata idAttr = propRepoMeta.entityMetadata().idAttribute();
String propertyRel = repository +
"." + entity.getClass().getSimpleName() +
"." + property;
if(propVal instanceof Collection) {
propertyRel += "." + propRepoMeta.entityMetadata().type().getSimpleName();
ResourceSet resources = new ResourceSet();
List<Resource<?>> outgoing = new ArrayList<Resource<?>>();
for(Object o : (Collection)propVal) {
String propValId = idAttr.get(o).toString();
URI path = buildUri(baseUri, repository, id, property, propValId);
if(shouldReturnLinks(accept)) {
resources.addLink(new ResourceLink(propertyRel, path));
links.add(new Link(path.toString(), propertyRel));
} 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);
EntityResource er = EntityResource.wrap(o, propRepoMeta, selfUri);
er.add(new Link(path.toString(), propertyRel));
outgoing.add(er);
}
}
body = resources;
// Run any post-processors for this domain type
for(ResourceSetPostProcessor pp : config.getResourceSetPostProcessors()) {
resources = pp.postProcess(request, resources);
}
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
new Resources(outgoing, links));
} else if(propVal instanceof Map) {
propertyRel += "." + propRepoMeta.entityMetadata().type().getSimpleName();
Map resource = new HashMap();
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);
@@ -1128,49 +1072,39 @@ public class RepositoryRestController
String sKey = objectToMapKey(oKey);
if(shouldReturnLinks(accept)) {
resource.put(sKey, new ResourceLink(propertyRel, path));
resource.put(sKey, new Link(path.toString(), propertyRel));
} else {
URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId);
Resource res = createResource(propRepoMeta.rel(),
entry.getValue(),
propRepoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(SELF, selfUri));
res.addLink(new ResourceLink(propertyRel, path));
resource.put(sKey, res);
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(propRepoMeta.domainType())) {
res = pp.postProcess(request, res);
}
EntityResource er = EntityResource.wrap(entry.getValue(), propRepoMeta, selfUri);
resource.put(sKey, er);
}
}
body = new MapResource(resource);
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
new Resource(resource, links));
} else {
String propValId = idAttr.get(propVal).toString();
URI path = buildUri(baseUri, repository, id, property);
URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId);
List<Resource<?>> outgoing = new ArrayList<Resource<?>>();
if(shouldReturnLinks(accept)) {
Resource<?> resource = new Resource<Object>();
resource.addLink(new ResourceLink(propertyRel, path));
body = resource;
links.add(new Link(path.toString(), propertyRel));
} else {
MapResource res = createResource(propRepoMeta.rel(),
propVal,
propRepoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(propertyRel, path));
res.addLink(new ResourceLink(SELF, selfUri));
body = res;
}
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(propRepoMeta.domainType())) {
body = pp.postProcess(request, (Resource)body);
EntityResource er = EntityResource.wrap(propVal, propRepoMeta, selfUri);
outgoing.add(er);
}
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
new Resources(outgoing, links));
}
publishEvent(new BeforeRenderResourceEvent(request, propRepoMeta, body));
return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), body);
}
/**
@@ -1269,13 +1203,13 @@ public class RepositoryRestController
};
MediaType incomingMediaType = request.getHeaders().getContentType();
LinkList incomingLinks = readIncoming(request,
Resource incomingLinks = readIncoming(request,
incomingMediaType,
LinkList.class);
Resource.class);
for(Link l : incomingLinks.getLinks()) {
Object o;
if(null != (o = domainObjectResolver.resolve(baseUri, URI.create(l.href().toString())))) {
rel.set(l.rel());
if(null != (o = domainObjectResolver.resolve(baseUri, URI.create(l.getHref())))) {
rel.set(l.getRel());
ResponseEntity<?> possibleResponse = entityHandler.handle(o);
if(null != possibleResponse) {
return possibleResponse;
@@ -1429,23 +1363,14 @@ public class RepositoryRestController
String propertyRel = repository + "." + repoMeta.entityMetadata().type().getSimpleName() + "." + property;
URI propertyPath = buildUri(baseUri, repository, id, property, linkedId);
URI selfUri = buildUri(baseUri, linkedRepoMeta.name(), linkedId);
Resource res = createResource(linkedRepoMeta.rel(),
linkedEntity,
linkedRepoMeta.entityMetadata(),
selfUri);
res.addLink(new ResourceLink(propertyRel, propertyPath));
res.addLink(new ResourceLink(SELF, selfUri));
publishEvent(new BeforeRenderResourcesEvent(request, repoMeta, res));
// Run any post-processors for this domain type
for(ResourcePostProcessor pp : config.getResourcePostProcessors(linkedRepoMeta.domainType())) {
res = pp.postProcess(request, res);
}
EntityResource er = EntityResource.wrap(linkedEntity, linkedRepoMeta, selfUri);
er.add(new Link(propertyPath.toString(), propertyRel));
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Location", selfUri.toString());
return negotiateResponse(request, HttpStatus.OK, headers, res);
return negotiateResponse(request, HttpStatus.OK, headers, er);
}
/**
@@ -1688,9 +1613,6 @@ public class RepositoryRestController
Internal helper methods
-----------------------------------
*/
private static URI buildUri(URI baseUri, String... pathSegments) {
return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri();
}
@SuppressWarnings({"unchecked"})
private void maybeAddPrevNextLink(URI resourceUri,
@@ -1706,7 +1628,7 @@ public class RepositoryRestController
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 ResourceLink(repoMeta.rel() + "." + rel, urib.build().toUri()));
links.add(new Link(urib.build().toUri().toString(), repoMeta.rel() + "." + rel));
}
}
@@ -1737,29 +1659,33 @@ public class RepositoryRestController
return (V)mappingHttpMessageConverter.read(targetType, request);
}
private MapResource createResource(String repoRel,
Object entity,
EntityMetadata<AttributeMetadata> entityMetadata,
URI baseUri) {
Map<String, Object> entityDto = new HashMap<String, Object>();
MapResource resource = new MapResource(entityDto);
for(Map.Entry<String, AttributeMetadata> attrMeta : entityMetadata.embeddedAttributes().entrySet()) {
String name = attrMeta.getKey();
Object val;
if(null != (val = attrMeta.getValue().get(entity))) {
entityDto.put(name, val);
}
}
for(String attrName : entityMetadata.linkedAttributes().keySet()) {
URI uri = buildUri(baseUri, attrName);
String rel = repoRel + "." + entity.getClass().getSimpleName() + "." + attrName;
resource.addLink(new ResourceLink(rel, uri));
}
return resource;
}
// private Set<Link> extractLinkedProperties(String repoRel,
// Object entity,
// EntityMetadata<AttributeMetadata> entityMetadata,
// URI baseUri) {
// Set<Link> links = new HashSet<Link>();
// for(String attrName : entityMetadata.linkedAttributes().keySet()) {
// URI uri = buildUri(baseUri, attrName);
// String rel = repoRel + "." + entity.getClass().getSimpleName() + "." + attrName;
// links.add(new Link(uri.toString(), rel));
// }
// return links;
// }
//
// private Map<String, Object> extractEmbeddedProperties(String repoRel,
// Object entity,
// EntityMetadata<AttributeMetadata> entityMetadata,
// URI baseUri) {
// Map<String, Object> entityDto = new HashMap<String, Object>();
// for(Map.Entry<String, AttributeMetadata> attrMeta : entityMetadata.embeddedAttributes().entrySet()) {
// String name = attrMeta.getKey();
// Object val;
// if(null != (val = attrMeta.getValue().get(entity))) {
// entityDto.put(name, val);
// }
// }
// return entityDto;
// }
private boolean shouldReturnLinks(String acceptHeader) {
if(null != acceptHeader) {

View File

@@ -2,10 +2,8 @@ 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;
/**
@@ -18,17 +16,11 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
*/
public class RepositoryRestHandlerAdapter extends ResourceProcessorInvokingHandlerAdapter {
@Autowired
private ResourcesReturnValueHandler resourcesReturnValueHandler;
public RepositoryRestHandlerAdapter(RepositoryRestConfiguration config) {
setCustomArgumentResolvers(Arrays.asList(
new ServerHttpRequestMethodArgumentResolver(),
new PagingAndSortingMethodArgumentResolver(config)
));
setCustomReturnValueHandlers(Arrays.<HandlerMethodReturnValueHandler>asList(
resourcesReturnValueHandler
));
}
@Override public int getOrder() {

View File

@@ -6,7 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.data.rest.repository.UriToDomainObjectResolver;
import org.springframework.data.rest.repository.UriToDomainObjectUriResolver;
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
@@ -44,7 +44,7 @@ public class RepositoryRestMvcConfiguration {
* Main configuration for the REST exporter.
*/
@Autowired(required = false)
protected RepositoryRestConfiguration repositoryRestConfig;
protected RepositoryRestConfiguration repositoryRestConfig = RepositoryRestConfiguration.DEFAULT;
/**
* For getting access to the {@link javax.persistence.EntityManagerFactory}.
@@ -62,7 +62,7 @@ public class RepositoryRestMvcConfiguration {
*/
@Bean public JpaRepositoryExporter jpaRepositoryExporter() {
return (null == customJpaRepositoryExporter
? new JpaRepositoryExporter().setDomainTypeMappings(repositoryRestConfiguration().getDomainTypeToRepositoryMappings())
? new JpaRepositoryExporter().setDomainTypeMappings(repositoryRestConfig.getDomainTypeToRepositoryMappings())
: customJpaRepositoryExporter);
}
@@ -88,20 +88,14 @@ public class RepositoryRestMvcConfiguration {
}
/**
* A {@link org.springframework.data.rest.core.Resolver} implementation that takes a {@link java.net.URI} and turns
* A {@link org.springframework.data.rest.core.UriResolver} implementation that takes a {@link java.net.URI} and turns
* it
* into a top-level domain object.
*
* @return
*/
@Bean public UriToDomainObjectResolver domainObjectResolver() {
return new UriToDomainObjectResolver();
}
@Bean public RepositoryRestConfiguration repositoryRestConfiguration() {
return (null == repositoryRestConfig
? RepositoryRestConfiguration.DEFAULT
: repositoryRestConfig);
@Bean public UriToDomainObjectUriResolver domainObjectResolver() {
return new UriToDomainObjectUriResolver();
}
/**
@@ -115,10 +109,6 @@ public class RepositoryRestMvcConfiguration {
return new RepositoryRestController();
}
@Bean ResourcesReturnValueHandler resourcesReturnValueHandler() {
return new ResourcesReturnValueHandler(repositoryRestConfiguration());
}
/**
* Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the
* {@link RepositoryRestController} class.
@@ -126,7 +116,7 @@ public class RepositoryRestMvcConfiguration {
* @return
*/
@Bean public RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() {
return new RepositoryRestHandlerAdapter(repositoryRestConfiguration());
return new RepositoryRestHandlerAdapter(repositoryRestConfig);
}
/**

View File

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

View File

@@ -425,7 +425,7 @@ public class ResourceProcessorHandlerMethodReturnValueHandler implements Handler
}
/**
* Helper extension of {@link AnnotationAwareOrderComparator} to make {@link #getOrder()} public to allow it being
* Helper extension of {@link AnnotationAwareOrderComparator} to make {@link #getOrder(Object)} public to allow it being
* used in a standalone fashion.
*
* @author Oliver Gierke

View File

@@ -1,13 +0,0 @@
package org.springframework.data.rest.webmvc;
import org.springframework.data.rest.core.PostProcessor;
import org.springframework.data.rest.core.ResourceSet;
/**
* 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 ResourceSetPostProcessor extends PostProcessor<ResourceSet> {
}

View File

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

View File

@@ -5,21 +5,19 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.rest.core.Link;
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.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
/**
* A special {@link org.springframework.http.converter.HttpMessageConverter} that can take various input formats and
@@ -35,51 +33,31 @@ public class UriListHttpMessageConverter extends AbstractHttpMessageConverter<Ob
@Override protected boolean supports(Class<?> clazz) {
return (RepositoryMethodResponse.class.isAssignableFrom(clazz)
|| List.class.isAssignableFrom(clazz)
|| Map.class.isAssignableFrom(clazz)
|| LinkList.class.isAssignableFrom(clazz)
|| Resource.class.isAssignableFrom(clazz));
|| Resource.class.isAssignableFrom(clazz)
|| Set.class.isAssignableFrom(clazz));
}
@SuppressWarnings({"unchecked"})
@Override
protected Object readInternal(Class<?> clazz,
HttpInputMessage inputMessage)
throws IOException,
HttpMessageNotReadableException {
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
Assert.isTrue((Resource.class.isAssignableFrom(clazz) || Set.class.isAssignableFrom(clazz)),
"Cannot read a text/uri-list into a " + clazz);
String rel = inputMessage.getHeaders().getFirst("x-spring-data-urilist-rel");
if(null == rel && inputMessage instanceof ServletServerHttpRequest) {
rel = ((ServletServerHttpRequest)inputMessage).getURI().getPath().substring(1).replaceAll("/", ".");
}
BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody()));
Set<Link> links = new HashSet<Link>();
String line;
Object links;
try {
links = clazz.newInstance();
} catch(InstantiationException e) {
throw new HttpMessageNotReadableException(e.getMessage(), e);
} catch(IllegalAccessException e) {
throw new HttpMessageNotReadableException(e.getMessage(), e);
}
while(null != (line = reader.readLine())) {
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(l);
} else if(links instanceof Map) {
List linksFromMap = (List)((Map)links).get("links");
if(null == linksFromMap) {
linksFromMap = new ArrayList();
((Map)links).put("links", linksFromMap);
}
linksFromMap.add(l);
} else if(links instanceof Resource) {
((Resource)links).addLink(l);
}
links.add(new Link(URI.create(line.trim()).toString(), rel));
}
return links;
return (Set.class.isAssignableFrom(clazz) ? links : new Resource<String>("", links));
}
@Override
@@ -87,22 +65,15 @@ public class UriListHttpMessageConverter extends AbstractHttpMessageConverter<Ob
throws IOException,
HttpMessageNotWritableException {
OutputStream body = outputMessage.getBody();
if(links instanceof LinkList) {
for(Link link : ((LinkList)links).getLinks()) {
body.write(link.href().toASCIIString().getBytes());
body.write('\n');
}
} else if(links instanceof List) {
for(Object o : (List)links) {
if(links instanceof Set) {
for(Object o : (Set)links) {
if(o instanceof Link) {
body.write(((Link)o).href().toASCIIString().getBytes());
body.write(((Link)o).getHref().getBytes());
} else {
body.write(o.toString().getBytes());
}
body.write('\n');
}
} else if(links instanceof Map) {
writeInternal(((Map)links).get("links"), outputMessage);
} else if(links instanceof RepositoryMethodResponse) {
writeInternal(((RepositoryMethodResponse)links).getLinks(), outputMessage);
} else if(links instanceof Resource) {

View File

@@ -44,8 +44,4 @@ class EventsSpec extends BaseSpec {
}
def "captures resource rendering events"() {
}
}

View File

@@ -29,7 +29,7 @@ class QueryMethodsSpec extends BaseSpec {
then:
response.statusCode == HttpStatus.OK
body.links.size() == 3
body.links.size() == 4
}

View File

@@ -0,0 +1,154 @@
package org.springframework.data.rest.webmvc.spec
import org.springframework.core.MethodParameter
import org.springframework.data.rest.test.webmvc.ApplicationConfig
import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration
import org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler
import org.springframework.hateoas.Link
import org.springframework.hateoas.Resource
import org.springframework.hateoas.ResourceProcessor
import org.springframework.test.context.ContextConfiguration
import org.springframework.web.method.support.HandlerMethodReturnValueHandler
import spock.lang.Specification
/**
* @author Jon Brisbin
*/
@ContextConfiguration(classes = [ApplicationConfig, RepositoryRestMvcConfiguration])
class ResourceProcessorSpec extends Specification {
static STRING_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createStringResource"), -1)
static LONG_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createLongResource"), -1)
static SPECIAL_STRING_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createSpecialStringResource"), -1)
static SPECIAL_LONG_RESOURCE_PARAM = new MethodParameter(ResourceProcessorSpec.getMethod("createSpecialLongResource"), -1)
HandlerMethodReturnValueHandler delegateHandler
List<ResourceProcessor<?>> processors = []
boolean handleReturnValueCalled
HandlerMethodReturnValueHandler resourceHandler
def setup() {
delegateHandler = Mock(HandlerMethodReturnValueHandler)
delegateHandler.handleReturnValue(_, _, null, null) >> { handleReturnValueCalled = true }
processors << new SpecialStringResourceProcessor() <<
new SpecialLongResourceProcessor() <<
new StringResourceProcessor() <<
new LongResourceProcessor()
resourceHandler = new ResourceProcessorHandlerMethodReturnValueHandler(delegateHandler, processors)
}
Resource<String> createStringResource() {
new Resource<String>("string-resource")
}
Resource<Long> createLongResource() {
new Resource<Long>(1L)
}
StringResource createSpecialStringResource() {
new StringResource("special-string-resource")
}
LongResource createSpecialLongResource() {
new LongResource(1L)
}
def "processes simple String resource"() {
given:
def resource = createStringResource()
when:
resourceHandler.handleReturnValue(resource, STRING_RESOURCE_PARAM, null, null)
then:
null != resource.getLink("string-resource")
handleReturnValueCalled
}
def "process simple Long resource"() {
given:
def resource = createLongResource()
when:
resourceHandler.handleReturnValue(resource, LONG_RESOURCE_PARAM, null, null)
then:
null != resource.getLink("long-resource")
handleReturnValueCalled
}
def "process specialized String resource"() {
given:
def resource = createSpecialStringResource()
when:
resourceHandler.handleReturnValue(resource, SPECIAL_STRING_RESOURCE_PARAM, null, null)
then:
null != resource.getLink("special-string-resource")
handleReturnValueCalled
}
def "process specialized Long resource"() {
given:
def resource = createSpecialLongResource()
when:
resourceHandler.handleReturnValue(resource, SPECIAL_LONG_RESOURCE_PARAM, null, null)
then:
null != resource.getLink("special-long-resource")
handleReturnValueCalled
}
}
class StringResourceProcessor implements ResourceProcessor<Resource<String>> {
@Override Resource<String> process(Resource<String> resource) {
resource.add(new Link("http://localhost:8080/string-resource", "string-resource"))
resource
}
}
class LongResourceProcessor implements ResourceProcessor<Resource<Long>> {
@Override Resource<Long> process(Resource<Long> resource) {
resource.add(new Link("http://localhost:8080/long-resource", "long-resource"))
resource
}
}
class StringResource extends Resource<String> {
StringResource(String content, Link... links) {
super(content, links)
}
}
class SpecialStringResourceProcessor implements ResourceProcessor<StringResource> {
@Override StringResource process(StringResource resource) {
resource.add(new Link("http://localhost:8080/special-string-resource", "special-string-resource"))
resource
}
}
class LongResource extends Resource<Long> {
LongResource(Long content, Link... links) {
super(content, links)
}
}
class SpecialLongResourceProcessor implements ResourceProcessor<LongResource> {
@Override LongResource process(LongResource resource) {
resource.add(new Link("http://localhost:8080/special-long-resource", "special-long-resource"))
resource
}
}

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.repository.annotation.ConvertWith;
import org.springframework.data.rest.repository.annotation.RestResource;
/**
@@ -30,8 +29,10 @@ public interface PersonRepository extends PagingAndSortingRepository<Person, Lon
@RestResource(path = "nameStartsWith", rel = "nameStartsWith")
Page findByNameStartsWith(@Param("name") String name, Pageable p);
@Query("select count(p) from Person p")
@RestResource(path = "count") Long personCount();
@Query("select p from Person p where p.id in(:ids)")
@RestResource(path = "id")
Page<Person> findById(@Param("ids") List<Long> ids, Pageable pageable);
@RestResource(path = "id") Page<Person> findById(@Param("ids") List<Long> ids, Pageable pageable);
}

View File

@@ -15,13 +15,6 @@
value="org.springframework.data.rest.test.webmvc.PersonRepository"/>
</map>
</property>
<property name="resourcePostProcessors">
<map key-type="java.lang.Class">
<entry key="org.springframework.data.rest.test.webmvc.Person">
<bean class="org.springframework.data.rest.test.webmvc.LoggingResourcePostProcessor"/>
</entry>
</map>
</property>
</bean>
<!--