From 3969c3494090eee1707a67d19bd88cf2d1c24abe Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 10 Jun 2014 16:57:46 +0200 Subject: [PATCH] DATAREST-317 - Support for excerpt projections. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a mechanism to define excerpt projections to be rendered for exposed repositories. The main user facing mechanism is the addition of the excerptProjection attribute to @RepositoryRestResource. This attribute takes a type which has to be a projection interface (see DATAREST-221 for more information about the general mechanism). If such a excerpt projection is in place, it will be used by default when rendering instances of the domain type in _embedded clauses or if it is related to. class Person { String name; int age; Person father; Person mother; Set siblings: } interface PersonExcerpt { String getName(); } If PersonExcerpt is now configured as excerpt projection for Person the collection resource for people will return: { _embedded : { people : [{ name : "Some name", _embedded : { mother : { name : "…" }, father : { name : "…" }, siblings : [ … ] }, _links : { self : { href : "…" }} }, … ] } } Here you can see how the age property is omitted when rendering a person in a collection. Also each person contains the excerpt projections of related resources and the links to them omitted. If you now follow the link to the item resource, you'll something like this: { name : "Some name", age : 34, _embedded : { mother : { name : "…" }, father : { name : "…" }, siblings : [ … ] }, _links : { self : { href : "…" }, mother : { href : "…" }, father : { href : "…" }, siblings : { href : "…" } } } Note, that age appears, as the representation is now rendered entirely. We also see the excerpts of related resource but also the links pointing to them in case you want to manage them. --- spring-data-rest-core/pom.xml | 2 +- .../annotation/RepositoryRestResource.java | 11 ++ .../mapping/CollectionResourceMapping.java | 8 + .../RepositoryAwareResourceInformation.java | 9 + .../RepositoryCollectionResourceMapping.java | 16 ++ .../TypeBasedCollectionResourceMapping.java | 9 + .../AbstractRepositoryRestController.java | 11 +- .../rest/webmvc/PersistentEntityResource.java | 167 ++++++++++++++++-- .../PersistentEntityResourceAssembler.java | 125 ++++++++++++- .../webmvc/RepositoryEntityController.java | 40 +++-- ...RepositoryPropertyReferenceController.java | 6 +- .../webmvc/RepositorySearchController.java | 2 +- ...tityResourceAssemblerArgumentResolver.java | 9 +- ...ResourceHandlerMethodArgumentResolver.java | 2 +- .../RepositoryRestMvcConfiguration.java | 15 +- .../json/PersistentEntityJackson2Module.java | 26 ++- .../webmvc/support/DefaultedPageable.java | 59 +++++++ ...PageableHandlerMethodArgumentResolver.java | 70 ++++++++ .../support/PersistentEntityProjector.java | 38 +++- .../PersistentEntityResourceProcessor.java | 6 +- .../data/rest/webmvc/support/Projector.java | 20 ++- .../webmvc/support/RepositoryEntityLinks.java | 30 +++- .../AbstractControllerIntegrationTests.java | 28 ++- .../webmvc/AbstractWebIntegrationTests.java | 7 +- .../PersistentEntityResourceUnitTests.java | 117 ++++++++++++ ...itoryEntityControllerIntegrationTests.java | 5 +- ...RepositoryRestHandlerMappingUnitTests.java | 4 +- .../data/rest/webmvc/jpa/Author.java | 2 +- .../data/rest/webmvc/jpa/BookExcerpt.java | 29 +++ .../data/rest/webmvc/jpa/BookRepository.java | 2 + .../rest/webmvc/jpa/DataRest262Tests.java | 5 +- .../data/rest/webmvc/jpa/JpaWebTests.java | 64 +++++-- .../data/rest/webmvc/jpa/OrderRepository.java | 2 + .../data/rest/webmvc/jpa/PersonSummary.java | 29 +++ .../PersistentEntitySerializationTests.java | 23 +-- .../PersistentEntityProjectorUnitTests.java | 6 +- ...RepositoryEntityLinksIntegrationTests.java | 20 ++- 37 files changed, 904 insertions(+), 120 deletions(-) create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageable.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageableHandlerMethodArgumentResolver.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceUnitTests.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookExcerpt.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonSummary.java diff --git a/spring-data-rest-core/pom.xml b/spring-data-rest-core/pom.xml index 6c0ea1946..d6048e53e 100644 --- a/spring-data-rest-core/pom.xml +++ b/spring-data-rest-core/pom.xml @@ -16,7 +16,7 @@ - 0.12.0.RELEASE + 0.13.0.BUILD-SNAPSHOT 1.1.0.RELEASE 1.1 diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RepositoryRestResource.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RepositoryRestResource.java index 1002294d7..0b3a70c65 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RepositoryRestResource.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/annotation/RepositoryRestResource.java @@ -72,4 +72,15 @@ public @interface RepositoryRestResource { * @return */ Description itemResourceDescription() default @Description(value = ""); + + /** + * Configures the projection type to be used when embedding item resources into collections and related resources. + * Defaults to {@link None}, which indicates full rendering of the items in a collection resource and no inlining of + * related resources. + * + * @return + */ + Class excerptProjection() default None.class; + + static class None {} } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CollectionResourceMapping.java index 57b88d91a..739edab2a 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CollectionResourceMapping.java @@ -35,4 +35,12 @@ public interface CollectionResourceMapping extends ResourceMapping { * @return */ ResourceDescription getItemResourceDescription(); + + /** + * Returns the projection type to be used when embedding item resources into collections and related resources. If + * {@literal null} is returned this will mean full rendering for collections and no rendering for related resources. + * + * @return + */ + Class getExcerptProjection(); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java index c9ac344f9..eb67dea6a 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceInformation.java @@ -169,6 +169,15 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { return mapping.getItemResourceDescription(); } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getExcerptProjection() + */ + @Override + public Class getExcerptProjection() { + return mapping.getExcerptProjection(); + } + /* * (non-Javadoc) * @see org.springframework.data.rest.core.mapping.ResourceMetadata#getSearchResourceMappings() diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java index 69f79a79e..d94442262 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java @@ -204,4 +204,20 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { return fallback; } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getExcerptProjection() + */ + @Override + public Class getExcerptProjection() { + + if (repositoryAnnotation == null) { + return null; + } + + Class excerptProjection = repositoryAnnotation.excerptProjection(); + + return excerptProjection.equals(RepositoryRestResource.None.class) ? null : excerptProjection; + } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java index ae9236d45..d3972f686 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/TypeBasedCollectionResourceMapping.java @@ -150,6 +150,15 @@ class TypeBasedCollectionResourceMapping implements CollectionResourceMapping { return fallback; } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.CollectionResourceMapping#getExcerptProjection() + */ + @Override + public Class getExcerptProjection() { + return null; + } + /** * Returns the default path to be used if the path is not configured manually. * diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java index 6d4d8ba9b..8d3047a58 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java @@ -200,24 +200,25 @@ class AbstractRepositoryRestController implements MessageSourceAware { } @SuppressWarnings({ "unchecked" }) - protected Resources resultToResources(Object result, PersistentEntityResourceAssembler assembler) { + protected Resources resultToResources(Object result, PersistentEntityResourceAssembler assembler, Link baseLink) { if (result instanceof Page) { Page page = (Page) result; - return entitiesToResources(page, assembler); + return entitiesToResources(page, assembler, baseLink); } else if (result instanceof Iterable) { return entitiesToResources((Iterable) result, assembler); } else if (null == result) { return new Resources(EMPTY_RESOURCE_LIST); } else { - Resource resource = assembler.toResource(result); + Resource resource = assembler.toFullResource(result); return new Resources(Collections.singletonList(resource)); } } protected Resources> entitiesToResources(Page page, - PersistentEntityResourceAssembler assembler) { - return pagedResourcesAssembler.toResource(page, assembler); + PersistentEntityResourceAssembler assembler, Link baseLink) { + return baseLink == null ? pagedResourcesAssembler.toResource(page, assembler) : pagedResourcesAssembler.toResource( + page, assembler, baseLink); } protected Resources> entitiesToResources(Iterable entities, diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java index 5c9408698..d73d72576 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java @@ -15,14 +15,19 @@ */ package org.springframework.data.rest.webmvc; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.hateoas.core.EmbeddedWrapper; +import org.springframework.util.Assert; -import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonUnwrapped; /** * A Spring HATEOAS {@link Resource} subclass that holds a reference to the entity's {@link PersistentEntity} metadata. @@ -30,25 +35,163 @@ import com.fasterxml.jackson.annotation.JsonIgnore; * @author Jon Brisbin * @author Oliver Gierke */ -public class PersistentEntityResource extends Resource { +public class PersistentEntityResource extends Resource { + + private static final Resources NO_EMBEDDEDS = new Resources( + Collections. emptyList()); private final PersistentEntity entity; + private final Resources embeddeds; + private final boolean enforceAssociationLinks; - public static PersistentEntityResource wrap(PersistentEntity entity, T obj, Link selfLink) { - return new PersistentEntityResource(entity, obj, selfLink); - } + /** + * Creates a new {@link PersistentEntityResource} for the given {@link PersistentEntity}, content, embedded + * {@link Resources}, links and flag whether to render all associations. + * + * @param entity must not be {@literal null}. + * @param content must not be {@literal null}. + * @param links must not be {@literal null}. + * @param renderAllAssociations + * @param embeddeds can be {@literal null}. + */ + private PersistentEntityResource(PersistentEntity entity, Object content, Iterable links, + boolean renderAllAssociations, Resources embeddeds) { - public PersistentEntityResource(PersistentEntity entity, T content, Link... links) { - this(entity, content, Arrays.asList(links)); - } - - private PersistentEntityResource(PersistentEntity entity, T content, Iterable links) { super(content, links); + + Assert.notNull(entity, "PersistentEntity must not be null!"); + this.entity = entity; + this.embeddeds = embeddeds == null ? NO_EMBEDDEDS : embeddeds; + this.enforceAssociationLinks = renderAllAssociations; } - @JsonIgnore + /** + * Returns the {@link PersistentEntity} for the underlying instance. + * + * @return + */ public PersistentEntity> getPersistentEntity() { return entity; } + + /** + * Returns the resources that are supposed to be rendered in the {@code _embedded} clause. + * + * @return the embeddeds + */ + @JsonUnwrapped + public Resources getEmbeddeds() { + return embeddeds; + } + + /** + * Returns whether the given {@link Link} shall be rendered for the resource. + * + * @param link must not be {@literal null}. + * @return + */ + public boolean shouldRenderLink(Link link) { + + Assert.notNull(link, "Link must not be null!"); + + if (enforceAssociationLinks) { + return true; + } + + for (EmbeddedWrapper wrapper : embeddeds) { + if (wrapper.hasRel(link.getRel())) { + return false; + } + } + + return true; + } + + /** + * Creates a new {@link Builder} to create {@link PersistentEntityResource}s eventually. + * + * @param content must not be {@literal null}. + * @param entity must not be {@literal null}. + * @return + */ + public static Builder build(Object content, PersistentEntity entity) { + return new Builder(content, entity); + } + + /** + * Builder to create {@link PersistentEntityResource} instances. + * + * @author Oliver Gierke + */ + public static class Builder { + + private final Object content; + private final PersistentEntity entity; + private final List links = new ArrayList(); + + private Resources embeddeds; + private boolean renderAllAssociationLinks = false; + + /** + * Creates a new {@link Builder} instance for the given content and {@link PersistentEntity}. + * + * @param content must not be {@literal null}. + * @param entity must not be {@literal null}. + */ + private Builder(Object content, PersistentEntity entity) { + + Assert.notNull(content, "Content must not be null!"); + Assert.notNull(entity, "PersistentEntity must not be null!"); + + this.content = content; + this.entity = entity; + } + + /** + * Configures the builder to embedd the given E + * + * @param resources can be {@literal null}. + * @return the builder + */ + public Builder withEmbedded(Iterable resources) { + + this.embeddeds = resources == null ? null : new Resources(resources); + return this; + } + + /** + * Configures the builder to render all association links independently of the embedded resources added. + * + * @return the builder + */ + public Builder renderAllAssociationLinks() { + + this.renderAllAssociationLinks = true; + return this; + } + + /** + * Adds the given {@link Link} to the {@link PersistentEntityResource}. + * + * @param link must not be {@literal null}. + * @return the builder + */ + public Builder withLink(Link link) { + + Assert.notNull(link, "Link must not be null!"); + + this.links.add(link); + return this; + } + + /** + * Finally creates the {@link PersistentEntityResource} instance. + * + * @return + */ + public PersistentEntityResource build() { + return new PersistentEntityResource(entity, content, links, renderAllAssociationLinks, embeddeds); + } + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java index 4dba53413..1d6b0c389 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java @@ -15,13 +15,26 @@ */ package org.springframework.data.rest.webmvc; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.SimpleAssociationHandler; import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.PersistentEntityResource.Builder; +import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.data.rest.webmvc.support.Projector; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; import org.springframework.hateoas.ResourceAssembler; +import org.springframework.hateoas.core.EmbeddedWrapper; +import org.springframework.hateoas.core.EmbeddedWrappers; import org.springframework.util.Assert; /** @@ -29,28 +42,34 @@ import org.springframework.util.Assert; * * @author Oliver Gierke */ -public class PersistentEntityResourceAssembler implements ResourceAssembler> { +public class PersistentEntityResourceAssembler implements ResourceAssembler { private final Repositories repositories; private final EntityLinks entityLinks; private final Projector projector; + private final ResourceMappings mappings; + private final EmbeddedWrappers wrappers = new EmbeddedWrappers(false); /** * Creates a new {@link PersistentEntityResourceAssembler}. * * @param repositories must not be {@literal null}. * @param entityLinks must not be {@literal null}. - * @param projections must not be {@literal null}. + * @param projector must not be {@literal null}. + * @param mappings must not be {@literal null}. */ - public PersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) { + public PersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector, + ResourceMappings mappings) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(entityLinks, "EntityLinks must not be null!"); Assert.notNull(projector, "PersistentEntityProjector must not be be null!"); + Assert.notNull(mappings, "ResourceMappings must not be null!"); this.repositories = repositories; this.entityLinks = entityLinks; this.projector = projector; + this.mappings = mappings; } /* @@ -58,10 +77,106 @@ public class PersistentEntityResourceAssembler implements ResourceAssembler toResource(Object instance) { + public PersistentEntityResource toResource(Object instance) { + + Assert.notNull(instance, "Entity instance must not be null!"); + + return wrap(projector.projectExcerpt(instance), instance).build(); + + } + + /** + * Returns the full object as {@link PersistentEntityResource} using the underlying {@link Projector}. + * + * @param instance must not be {@literal null}. + * @return + */ + public PersistentEntityResource toFullResource(Object instance) { + + Assert.notNull(instance, "Entity instance must not be null!"); + return wrap(projector.project(instance), instance).// + renderAllAssociationLinks().build(); + } + + private Builder wrap(Object instance, Object source) { + + PersistentEntity entity = repositories.getPersistentEntity(source.getClass()); + + return PersistentEntityResource.build(instance, entity).// + withEmbedded(getEmbeddedResources(source)).// + withLink(getSelfLinkFor(source)); + } + + /** + * Returns the embedded resources to render. This will add an {@link RelatedResource} for linkable associations if + * they have an excerpt projection registered. + * + * @param instance must not be {@literal null}. + * @return + */ + private Iterable getEmbeddedResources(Object instance) { + + Assert.notNull(instance, "Entity instance must not be null!"); PersistentEntity entity = repositories.getPersistentEntity(instance.getClass()); - return PersistentEntityResource.wrap(entity, projector.project(instance), getSelfLinkFor(instance)); + + final List associationProjections = new ArrayList(); + final BeanWrapper wrapper = BeanWrapper.create(instance, null); + final AssociationLinks associationLinks = new AssociationLinks(mappings); + final ResourceMetadata metadata = mappings.getMappingFor(instance.getClass()); + + entity.doWithAssociations(new SimpleAssociationHandler() { + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association) + */ + @Override + public void doWithAssociation(Association> association) { + + PersistentProperty property = association.getInverse(); + + if (!associationLinks.isLinkableAssociation(property)) { + return; + } + + if (!projector.hasExcerptProjection(property.getActualType())) { + return; + } + + Object value = wrapper.getProperty(association.getInverse()); + + if (value == null) { + return; + } + + String rel = metadata.getMappingFor(property).getRel(); + + if (value instanceof Collection) { + + Collection collection = (Collection) value; + + if (collection.isEmpty()) { + return; + } + + List nestedCollection = new ArrayList(); + + for (Object element : collection) { + if (element != null) { + nestedCollection.add(projector.projectExcerpt(element)); + } + } + + associationProjections.add(wrappers.wrap(nestedCollection, rel)); + + } else { + associationProjections.add(wrappers.wrap(projector.projectExcerpt(value), rel)); + } + } + }); + + return associationProjections; } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index 35ab33eac..551a18374 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.convert.ConversionService; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.repository.support.Repositories; @@ -45,8 +44,9 @@ import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.rest.core.support.DomainObjectMerger; import org.springframework.data.rest.core.support.DomainObjectMerger.NullHandlingPolicy; import org.springframework.data.rest.webmvc.support.BackendId; +import org.springframework.data.rest.webmvc.support.DefaultedPageable; +import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.data.web.PagedResourcesAssembler; -import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.Resource; @@ -72,7 +72,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem private static final String BASE_MAPPING = "/{repository}"; - private final EntityLinks entityLinks; + private final RepositoryEntityLinks entityLinks; private final RepositoryRestConfiguration config; private final ConversionService conversionService; private final DomainObjectMerger domainObjectMerger; @@ -81,7 +81,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @Autowired public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config, - EntityLinks entityLinks, PagedResourcesAssembler assembler, + RepositoryEntityLinks entityLinks, PagedResourcesAssembler assembler, @Qualifier("defaultConversionService") ConversionService conversionService, DomainObjectMerger domainObjectMerger) { super(assembler); @@ -103,9 +103,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) - public Resources getCollectionResource(final RootResourceInformation resourceInformation, Pageable pageable, - Sort sort, PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException, - HttpRequestMethodNotSupportedException { + public Resources getCollectionResource(final RootResourceInformation resourceInformation, + DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) + throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.COLLECTION); @@ -118,7 +118,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem Iterable results; if (pageable != null) { - results = invoker.invokeFindAll(pageable); + results = invoker.invokeFindAll(pageable.getPageable()); } else { results = invoker.invokeFindAll(sort); } @@ -132,7 +132,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem .withRel(searchMappings.getRel())); } - Resources resources = resultToResources(results, assembler); + Link baseLink = entityLinks.linkToPagedResource(resourceInformation.getDomainType(), pageable.isDefault() ? null + : pageable.getPageable()); + + Resources resources = resultToResources(results, assembler, baseLink); resources.add(links); return resources; } @@ -141,15 +144,15 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @SuppressWarnings({ "unchecked" }) @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/x-spring-data-compact+json", "text/uri-list" }) - public Resources getCollectionResourceCompact(RootResourceInformation repoRequest, Pageable pageable, Sort sort, - PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException, + public Resources getCollectionResourceCompact(RootResourceInformation repoRequest, DefaultedPageable pageable, + Sort sort, PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { Resources resources = getCollectionResource(repoRequest, pageable, sort, assembler); List links = new ArrayList(resources.getLinks()); for (Resource resource : ((Resources>) resources).getContent()) { - PersistentEntityResource persistentEntityResource = (PersistentEntityResource) resource; + PersistentEntityResource persistentEntityResource = (PersistentEntityResource) resource; links.add(resourceLink(repoRequest, persistentEntityResource)); } if (resources instanceof PagedResources) { @@ -170,7 +173,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST) public ResponseEntity postCollectionResource(RootResourceInformation resourceInformation, - PersistentEntityResource payload, PersistentEntityResourceAssembler assembler) + PersistentEntityResource payload, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.POST, ResourceType.COLLECTION); @@ -205,7 +208,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem return new ResponseEntity>(HttpStatus.NOT_FOUND); } - return new ResponseEntity>(assembler.toResource(domainObj), HttpStatus.OK); + return new ResponseEntity>(assembler.toFullResource(domainObj), HttpStatus.OK); } /** @@ -219,7 +222,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT) public ResponseEntity putItemResource(RootResourceInformation resourceInformation, - PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler) + PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM); @@ -250,7 +253,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PATCH) public ResponseEntity patchItemResource(RootResourceInformation resourceInformation, - PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler) + PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM); @@ -323,7 +326,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem } if (config.isReturnBodyOnUpdate()) { - return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, assembler.toResource(obj)); + return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, assembler.toFullResource(obj)); } else { return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT, headers); } @@ -346,8 +349,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem HttpHeaders headers = new HttpHeaders(); addLocationHeader(headers, assembler, savedObject); - PersistentEntityResource resource = config.isReturnBodyOnCreate() ? assembler.toResource(savedObject) - : null; + PersistentEntityResource resource = config.isReturnBodyOnCreate() ? assembler.toFullResource(savedObject) : null; return ControllerUtils.toResponseEntity(HttpStatus.CREATED, headers, resource); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index e84e3f8e6..8a3dd9f5c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -138,7 +138,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro } else { - PersistentEntityResource resource = assembler.toResource(prop.propertyValue); + PersistentEntityResource resource = assembler.toResource(prop.propertyValue); headers.set("Content-Location", resource.getId().getHref()); return resource; } @@ -212,7 +212,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro if (propertyId.equals(sId)) { - PersistentEntityResource resource = assembler.toResource(obj); + PersistentEntityResource resource = assembler.toResource(obj); headers.set("Content-Location", resource.getId().getHref()); return resource; } @@ -225,7 +225,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro if (propertyId.equals(sId)) { - PersistentEntityResource resource = assembler.toResource(entry.getValue()); + PersistentEntityResource resource = assembler.toResource(entry.getValue()); headers.set("Content-Location", resource.getId().getHref()); return resource; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index 605455a92..7cbd79c88 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -219,7 +219,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { return result; } - return resultToResources(result, assembler); + return resultToResources(result, assembler, null); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java index 21efa127f..238f62511 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java @@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc.config; import org.springframework.core.MethodParameter; import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.projection.ProjectionDefinitions; import org.springframework.data.rest.core.projection.ProjectionFactory; import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler; @@ -39,6 +40,7 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle private final EntityLinks entityLinks; private final ProjectionDefinitions projectionDefinitions; private final ProjectionFactory projectionFactory; + private final ResourceMappings mappings; /** * Creates a new {@link PersistentEntityResourceAssemblerArgumentResolver} for the given {@link Repositories}, @@ -50,7 +52,7 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle * @param projectionFactory must not be {@literal null}. */ public PersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks, - ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) { + ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory, ResourceMappings mappings) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(entityLinks, "EntityLinks must not be null!"); @@ -61,6 +63,7 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle this.entityLinks = entityLinks; this.projectionDefinitions = projectionDefinitions; this.projectionFactory = projectionFactory; + this.mappings = mappings; } /* @@ -82,8 +85,8 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName()); PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory, - projectionParameter); + projectionParameter, mappings); - return new PersistentEntityResourceAssembler(repositories, entityLinks, projector); + return new PersistentEntityResourceAssembler(repositories, entityLinks, projector, mappings); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java index 236993ec9..8f0411812 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java @@ -103,7 +103,7 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, domainType, converter)); } - return new PersistentEntityResource(resourceInformation.getPersistentEntity(), obj); + return PersistentEntityResource.build(obj, resourceInformation.getPersistentEntity()).build(); } throw new HttpMessageNotReadableException(String.format(NO_CONVERTER_FOUND, domainType, contentType)); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 21271a0ad..4a1a34ef8 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -75,6 +75,7 @@ import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaCon import org.springframework.data.rest.webmvc.spi.BackendIdConverter; import org.springframework.data.rest.webmvc.spi.BackendIdConverter.DefaultIdConverter; import org.springframework.data.rest.webmvc.support.BackendIdHandlerMethodArgumentResolver; +import org.springframework.data.rest.webmvc.support.DefaultedPageableHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.support.JpaHelper; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; @@ -85,7 +86,6 @@ import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver; import org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration; import org.springframework.data.web.config.SpringDataJacksonConfiguration; import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.ResourceProcessor; @@ -309,7 +309,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon * @throws Exception */ @Bean - public EntityLinks entityLinks() { + public RepositoryEntityLinks entityLinks() { return new RepositoryEntityLinks(repositories(), resourceMappings(), config(), pageableResolver(), backendIdConverterRegistry()); } @@ -573,10 +573,15 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon private List defaultMethodArgumentResolvers() { PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver( - repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory)); + repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory), + resourceMappings()); - return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(), - repoRequestArgumentResolver(), persistentEntityArgumentResolver(), + HateoasPageableHandlerMethodArgumentResolver pageableResolver = pageableResolver(); + HandlerMethodArgumentResolver defaultedPageableResolver = new DefaultedPageableHandlerMethodArgumentResolver( + pageableResolver); + + return Arrays.asList(defaultedPageableResolver, pageableResolver, sortResolver(), + serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(), resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE, peraResolver, backendIdHandlerMethodArgumentResolver()); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index 21e5de9ea..5979e812c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -38,9 +38,11 @@ import org.springframework.data.rest.webmvc.mapping.AssociationLinks; import org.springframework.data.rest.webmvc.mapping.LinkCollectingAssociationHandler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; import org.springframework.hateoas.UriTemplate; import org.springframework.util.Assert; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -111,7 +113,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { * * @author Oliver Gierke */ - private static class PersistentEntityResourceSerializer extends StdSerializer> { + private static class PersistentEntityResourceSerializer extends StdSerializer { private final PersistentEntities entities; private final AssociationLinks associationLinks; @@ -140,7 +142,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) */ @Override - public void serialize(final PersistentEntityResource resource, final JsonGenerator jgen, + public void serialize(final PersistentEntityResource resource, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonGenerationException { if (LOG.isDebugEnabled()) { @@ -153,16 +155,28 @@ public class PersistentEntityJackson2Module extends SimpleModule { throw new JsonGenerationException(String.format("No self link found resource %s!", resource)); } + List links = new ArrayList(); + links.addAll(resource.getLinks()); + Path basePath = new Path(id.expand().getHref()); LinkCollectingAssociationHandler associationHandler = new LinkCollectingAssociationHandler(entities, basePath, associationLinks); resource.getPersistentEntity().doWithAssociations(associationHandler); - List links = new ArrayList(); - links.addAll(resource.getLinks()); - links.addAll(associationHandler.getLinks()); + for (Link link : associationHandler.getLinks()) { + if (resource.shouldRenderLink(link)) { + links.add(link); + } + } + + Resource resourceToRender = new Resource(resource.getContent(), links) { + + @JsonUnwrapped + public Resources getEmbedded() { + return resource.getEmbeddeds(); + } + }; - Resource resourceToRender = new Resource(resource.getContent(), links); provider.defaultSerializeValue(resourceToRender, jgen); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageable.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageable.java new file mode 100644 index 000000000..8d6b41d2f --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageable.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.support; + +import org.springframework.data.domain.Pageable; + +/** + * Value object to capture a {@link Pageable} as well it is the default one configured. + * + * @author Oliver Gierke + */ +public class DefaultedPageable { + + private final Pageable pageable; + private final boolean isDefault; + + /** + * Creates a new {@link DefaultedPageable} with the given {@link Pageable} and default flag. + * + * @param pageable can be {@literal null}. + * @param isDefault + */ + DefaultedPageable(Pageable pageable, boolean isDefault) { + + this.pageable = pageable; + this.isDefault = isDefault; + } + + /** + * Returns the delegate {@link Pageable}. + * + * @return can be {@literal null}. + */ + public Pageable getPageable() { + return pageable; + } + + /** + * Returns whether the contained {@link Pageable} is the default one configured. + * + * @return the isDefault + */ + public boolean isDefault() { + return isDefault; + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageableHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageableHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..a595bc644 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageableHandlerMethodArgumentResolver.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.support; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * {@link HandlerMethodArgumentResolver} to resolve {@link DefaultedPageable} if requested. Allows to find out whether + * the resolved {@link Pageable} is logically identical to the fallback on configured on the delegate + * {@link PageableHandlerMethodArgumentResolver}. + * + * @author Oliver Gierke + */ +public class DefaultedPageableHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final PageableHandlerMethodArgumentResolver resolver; + + /** + * Creates a new {@link DefaultedPageableHandlerMethodArgumentResolver} delegating to the given + * {@link PageableHandlerMethodArgumentResolver}. + * + * @param resolver must not be {@literal null}. + */ + public DefaultedPageableHandlerMethodArgumentResolver(PageableHandlerMethodArgumentResolver resolver) { + + Assert.notNull(resolver, "PageableHandlerMethodArgumentResolver must not be null!"); + this.resolver = resolver; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) + */ + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + Pageable pageable = resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + return new DefaultedPageable(pageable, resolver.isFallbackPageable(pageable)); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return DefaultedPageable.class.isAssignableFrom(parameter.getParameterType()); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java index b8e2cc650..5502c21fd 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjector.java @@ -15,6 +15,8 @@ */ package org.springframework.data.rest.webmvc.support; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.projection.ProjectionDefinitions; import org.springframework.data.rest.core.projection.ProjectionFactory; import org.springframework.util.Assert; @@ -30,6 +32,7 @@ public class PersistentEntityProjector implements Projector { private final ProjectionDefinitions projectionDefinitions; private final ProjectionFactory factory; private final String projection; + private final ResourceMappings mappings; /** * Creates a new {@link PersistentEntityProjector} using the given {@link ProjectionDefinitions}, @@ -40,7 +43,7 @@ public class PersistentEntityProjector implements Projector { * @param projection can be empty. */ public PersistentEntityProjector(ProjectionDefinitions projectionDefinitions, ProjectionFactory factory, - String projection) { + String projection, ResourceMappings mappings) { Assert.notNull(projectionDefinitions, "ProjectionDefinitions must not be null!"); Assert.notNull(factory, "ProjectionFactory must not be null!"); @@ -48,6 +51,7 @@ public class PersistentEntityProjector implements Projector { this.projectionDefinitions = projectionDefinitions; this.factory = factory; this.projection = projection; + this.mappings = mappings; } /* @@ -56,6 +60,8 @@ public class PersistentEntityProjector implements Projector { */ public Object project(Object source) { + Assert.notNull(source, "Projection source must not be null!"); + if (!StringUtils.hasText(projection)) { return source; } @@ -63,4 +69,34 @@ public class PersistentEntityProjector implements Projector { Class projectionType = projectionDefinitions.getProjectionType(source.getClass(), projection); return projectionType == null ? source : factory.createProjection(source, projectionType); } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.Projector#projectExcerpt(java.lang.Object) + */ + @Override + public Object projectExcerpt(Object source) { + + Assert.notNull(source, "Projection source must not be null!"); + + ResourceMetadata metadata = mappings.getMappingFor(source.getClass()); + Class projection = metadata == null ? null : metadata.getExcerptProjection(); + + if (projection == null) { + return project(source); + } + + return projection == null ? source : factory.createProjection(source, projection); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.support.Projector#hasExcerptProjection(java.lang.Class) + */ + @Override + public boolean hasExcerptProjection(Class type) { + + ResourceMetadata metadata = mappings.getMappingFor(type); + return metadata == null ? false : metadata.getExcerptProjection() != null; + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityResourceProcessor.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityResourceProcessor.java index 0096108ce..88d32ec2e 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityResourceProcessor.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/PersistentEntityResourceProcessor.java @@ -16,7 +16,7 @@ import org.springframework.hateoas.ResourceProcessor; /** * @author Jon Brisbin */ -public class PersistentEntityResourceProcessor implements ResourceProcessor> { +public class PersistentEntityResourceProcessor implements ResourceProcessor { private final List resourceProcessors = new ArrayList(); @@ -35,8 +35,10 @@ public class PersistentEntityResourceProcessor implements ResourceProcessor process(PersistentEntityResource resource) { + public PersistentEntityResource process(PersistentEntityResource resource) { + Object content = resource.getContent(); + if (null == content) { return resource; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java index 271640677..f5c6fb5b6 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/Projector.java @@ -29,5 +29,23 @@ public interface Projector { * @param source must not be {@literal null}. * @return */ - public Object project(Object source); + Object project(Object source); + + /** + * Creates a excerpt projection for the given source. If no excerpt projection is available, the call will fall back + * to the behavior of {@link #project(Object)}. If you completely wish to skip handling the object, check for the + * presence of an excerpt projection using {@link #hasExcerptProjection(Class)}. + * + * @param source must not be {@literal null}. + * @return + */ + Object projectExcerpt(Object source); + + /** + * Returns whether an excerpt projection has been registered for the given type. + * + * @param type must not be {@literal null}. + * @return + */ + boolean hasExcerptProjection(Class type); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index 0e2429bda..ae4f9fb4d 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -20,6 +20,7 @@ import static org.springframework.hateoas.TemplateVariable.VariableType.*; import java.io.Serializable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; @@ -37,7 +38,6 @@ import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.core.AbstractEntityLinks; import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.Assert; -import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -111,20 +111,23 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { return linkFor(type); } - /* - * (non-Javadoc) - * @see org.springframework.hateoas.EntityLinks#linkToCollectionResource(java.lang.Class) - */ - @Override - public Link linkToCollectionResource(Class type) { + public Link linkToPagedResource(Class type, Pageable pageable) { ResourceMetadata metadata = mappings.getMappingFor(type); TemplateVariables variables = new TemplateVariables(); String href = linkFor(type).withSelfRel().getHref(); if (metadata.isPagingResource()) { - UriComponents components = UriComponentsBuilder.fromUriString(href).build(); - variables = variables.concat(resolver.getPaginationTemplateVariables(null, components)); + + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(href); + + if (pageable != null) { + resolver.enhance(builder, null, pageable); + } + + href = builder.build().toString(); + + variables = variables.concat(resolver.getPaginationTemplateVariables(null, builder.build())); } ProjectionDefinitionConfiguration projectionConfiguration = config.projectionConfiguration(); @@ -137,6 +140,15 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { variables), metadata.getRel()); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkToCollectionResource(java.lang.Class) + */ + @Override + public Link linkToCollectionResource(Class type) { + return linkToPagedResource(type, null); + } + /* * (non-Javadoc) * @see org.springframework.hateoas.EntityLinks#linkToSingleResource(java.lang.Class, java.lang.Object) diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java index d96869552..c06d90d12 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractControllerIntegrationTests.java @@ -53,12 +53,8 @@ public abstract class AbstractControllerIntegrationTests { @Bean public PersistentEntityResourceAssembler persistentEntityResourceAssembler() { - return new PersistentEntityResourceAssembler(repositories(), entityLinks(), new Projector() { - @Override - public Object project(Object source) { - return source; - } - }); + return new PersistentEntityResourceAssembler(repositories(), entityLinks(), StubProjector.INSTANCE, + resourceMappings()); } } @@ -101,4 +97,24 @@ public abstract class AbstractControllerIntegrationTests { protected ResourceMetadata getMetadata(Class domainType) { return mappings.getMappingFor(domainType); } + + private static enum StubProjector implements Projector { + + INSTANCE; + + @Override + public Object project(Object source) { + return source; + } + + @Override + public Object projectExcerpt(Object source) { + return source; + } + + @Override + public boolean hasExcerptProjection(Class type) { + return false; + } + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java index 123fa3958..e895e8cf0 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java @@ -260,8 +260,11 @@ public abstract class AbstractWebIntegrationTests { @SuppressWarnings("unchecked") protected T assertHasJsonPathValue(String path, MockHttpServletResponse response) throws Exception { - Object jsonPathResult = JsonPath.read(response.getContentAsString(), path); - assertThat(jsonPathResult, is(notNullValue())); + String content = response.getContentAsString(); + Object jsonPathResult = JsonPath.read(content, path); + + assertThat(String.format("JSONPath lookup for %s did return null in %s.", path, content), jsonPathResult, + is(notNullValue())); if (jsonPathResult instanceof JSONArray) { JSONArray array = (JSONArray) jsonPathResult; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceUnitTests.java new file mode 100644 index 000000000..c7e13958a --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/PersistentEntityResourceUnitTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resources; +import org.springframework.hateoas.core.EmbeddedWrapper; +import org.springframework.hateoas.core.EmbeddedWrappers; + +/** + * Unit tests for {@link PersistentEntityResource}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class PersistentEntityResourceUnitTests { + + @Mock Object payload; + @Mock PersistentEntity entity; + + Resources resources; + Link link = new Link("http://localhost", "foo"); + + @Before + public void setUp() { + + EmbeddedWrappers wrappers = new EmbeddedWrappers(false); + EmbeddedWrapper wrapper = wrappers.wrap("Embedded", "foo"); + this.resources = new Resources(Collections.singleton(wrapper)); + } + + /** + * @see DATAREST-317 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullPayload() { + PersistentEntityResource.build(null, entity); + } + + /** + * @see DATAREST-317 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullPersistentEntity() { + PersistentEntityResource.build(payload, null); + } + + /** + * @see DATAREST-317 + */ + @Test + public void defaultsEmbeddedsToEmptyResources() { + + PersistentEntityResource resource = PersistentEntityResource.build(payload, entity).build(); + + assertThat(resource.getEmbeddeds(), is(notNullValue())); + assertThat(resource.getEmbeddeds(), is(emptyIterable())); + } + + /** + * @see DATAREST-317 + */ + @Test + public void doesNotRenderAssociationLinksIfEmbeddedWithRelPresent() { + + PersistentEntityResource resource = PersistentEntityResource.build(payload, entity).// + withEmbedded(resources).build(); + + assertThat(resource.shouldRenderLink(link), is(false)); + } + + /** + * @see DATAREST-317 + */ + @Test + public void rendersAssociationLinksIfEvenIfEmbeddedWithRelPresentButLinksEnforced() { + + PersistentEntityResource resource = PersistentEntityResource.build(payload, entity).// + withEmbedded(resources).renderAllAssociationLinks().build(); + + assertThat(resource.shouldRenderLink(link), is(true)); + } + + /** + * @see DATAREST-317 + */ + @Test + public void rendersAssociationIfNoEmbeddedPresent() { + + PersistentEntityResource resource = PersistentEntityResource.build(payload, entity).build(); + assertThat(resource.shouldRenderLink(link), is(true)); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java index 373ac67a7..11caef5c0 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java @@ -77,8 +77,9 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll public void setsExpandedSelfUriInLocationHeader() throws Exception { RootResourceInformation information = getResourceInformation(Order.class); - PersistentEntityResource persistentEntityResource = new PersistentEntityResource( - entities.getPersistentEntity(Order.class), new Order(new Person())); + + PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()), + entities.getPersistentEntity(Order.class)).build(); ResponseEntity entity = controller.putItemResource(information, persistentEntityResource, 1L, assembler); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java index a3d2a903a..d2760c919 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java @@ -29,11 +29,11 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.data.rest.webmvc.support.DefaultedPageable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.method.HandlerMethod; @@ -66,7 +66,7 @@ public class RepositoryRestHandlerMappingUnitTests { mockRequest = new MockHttpServletRequest(); listEntitiesMethod = RepositoryEntityController.class.getMethod("getCollectionResource", - RootResourceInformation.class, Pageable.class, Sort.class, PersistentEntityResourceAssembler.class); + RootResourceInformation.class, DefaultedPageable.class, Sort.class, PersistentEntityResourceAssembler.class); rootHandlerMethod = RepositoryController.class.getMethod("listRepositories"); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Author.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Author.java index 017539610..b6ca61e4d 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Author.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Author.java @@ -28,7 +28,7 @@ public class Author { @Id @GeneratedValue// Long id; - String name; + public String name; @ManyToMany(mappedBy = "authors")// Set books = new HashSet(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookExcerpt.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookExcerpt.java new file mode 100644 index 000000000..ca75c35bf --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookExcerpt.java @@ -0,0 +1,29 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.jpa; + +import org.springframework.data.rest.core.config.Projection; + +/** + * Interface for an excerpt projection for {@link Book}s. + * + * @author Oliver Gierke + */ +@Projection(name = "excerpt", types = Book.class) +public interface BookExcerpt { + + String getTitle(); +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java index 679b9b97d..17ca86201 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java @@ -16,10 +16,12 @@ package org.springframework.data.rest.webmvc.jpa; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; /** * @author Oliver Gierke */ +@RepositoryRestResource(excerptProjection = BookExcerpt.class) public interface BookRepository extends CrudRepository { } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java index 35aea2b35..cd3a887bc 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest262Tests.java @@ -111,8 +111,9 @@ public class DataRest262Tests { JpaPersistentEntity persistentEntity = mappingContext.getPersistentEntity(AircraftMovement.class); - Resource resource = PersistentEntityResource.wrap(persistentEntity, movement, new Link( - "/api/airports/" + movement.id)); + Resource resource = PersistentEntityResource.build(movement, persistentEntity).// + withLink(new Link("/api/airports/" + movement.id)).// + build(); String result = mapper.writeValueAsString(resource); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index a90669b37..5e0463a70 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -143,13 +143,12 @@ public class JpaWebTests extends AbstractWebIntegrationTests { * @see DATAREST-169 */ @Test - public void exposesCreatorOfAnOrder() throws Exception { + public void exposesLinkForRelatedResource() throws Exception { MockHttpServletResponse response = request("/"); Link ordersLink = assertHasLinkWithRel("orders", response); MockHttpServletResponse orders = request(ordersLink); - Link creatorLink = assertHasContentLinkWithRel("creator", orders); assertThat(request(creatorLink), is(notNullValue())); @@ -252,20 +251,12 @@ public class JpaWebTests extends AbstractWebIntegrationTests { @Test public void listsSiblingsWithContentCorrectly() throws Exception { - - MockHttpServletResponse response = mvc.perform(get("/people")).andReturn().getResponse(); - String href = assertHasJsonPathValue(String.format(LINK_TO_SIBLINGS_OF, "John"), response); - - mvc.perform(get(href)).andExpect(status().isOk()); + assertPersonWithNameAndSiblingLink("John"); } @Test public void listsEmptySiblingsCorrectly() throws Exception { - - MockHttpServletResponse response = mvc.perform(get("/people")).andReturn().getResponse(); - String href = assertHasJsonPathValue(String.format(LINK_TO_SIBLINGS_OF, "Billy Bob"), response); - - mvc.perform(get(href)).andExpect(status().isOk()); + assertPersonWithNameAndSiblingLink("Billy Bob"); } /** @@ -319,9 +310,9 @@ public class JpaWebTests extends AbstractWebIntegrationTests { Link frodosSiblingsLink = links.get(0); - putAndGet(frodosSiblingsLink, links.get(1).getHref(), TEXT_URI_LIST); - putAndGet(frodosSiblingsLink, links.get(2).getHref(), TEXT_URI_LIST); - putAndGet(frodosSiblingsLink, links.get(3).getHref(), TEXT_URI_LIST); + putAndGet(frodosSiblingsLink, links.get(1).expand().getHref(), TEXT_URI_LIST); + putAndGet(frodosSiblingsLink, links.get(2).expand().getHref(), TEXT_URI_LIST); + putAndGet(frodosSiblingsLink, links.get(3).expand().getHref(), TEXT_URI_LIST); assertSiblingNames(frodosSiblingsLink, "Pippin"); patchAndGet(frodosSiblingsLink, links.get(2).getHref(), TEXT_URI_LIST); @@ -524,6 +515,31 @@ public class JpaWebTests extends AbstractWebIntegrationTests { assertThat(JsonPath. read(responseBody, "$.content"), hasSize(0)); } + /** + * @see DATAREST-317 + */ + @Test + public void rendersExcerptProjectionsCorrectly() throws Exception { + + Link authorsLink = discoverUnique("authors"); + + MockHttpServletResponse response = request(authorsLink); + String firstAuthorPath = "$._embedded.authors[0]"; + + // Has main content + assertHasJsonPathValue(firstAuthorPath.concat(".name"), response); + + // Embeddes content of related entity but no link to it + assertHasJsonPathValue(firstAuthorPath.concat("._embedded.books[0].title"), response); + assertJsonPathDoesntExist(firstAuthorPath.concat("._links.books"), response); + + // Access item resource and expect link to related resource present + String content = response.getContentAsString(); + String href = JsonPath.read(content, firstAuthorPath.concat("._links.self.href")); + + follow(new Link(href)).andExpect(hasLinkWithRel("books")); + } + /** * Asserts the {@link Person} resource the given link points to contains siblings with the given names. * @@ -540,6 +556,24 @@ public class JpaWebTests extends AbstractWebIntegrationTests { assertThat(persons, hasItems(siblingNames)); } + private void assertPersonWithNameAndSiblingLink(String name) throws Exception { + + MockHttpServletResponse response = request(discoverUnique("people")); + + String jsonPath = String.format("$._embedded.people[?(@.firstName == '%s')][0]", name); + + // Assert content inlined + Object john = JsonPath.read(response.getContentAsString(), jsonPath); + assertThat(john, is(notNullValue())); + assertThat(JsonPath.read(john, "$.firstName"), is(notNullValue())); + + // Assert sibling link exposed in resource pointed to + Link selfLink = new Link(JsonPath. read(john, "$._links.self.href")); + follow(selfLink).// + andExpect(status().isOk()).// + andExpect(jsonPath("$._links.siblings", is(notNullValue()))); + } + private static String toUriList(Link... links) { List uris = new ArrayList(links.length); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderRepository.java index 0d2fab326..e1fd96085 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/OrderRepository.java @@ -16,10 +16,12 @@ package org.springframework.data.rest.webmvc.jpa; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; /** * @author Oliver Gierke */ +@RepositoryRestResource public interface OrderRepository extends CrudRepository { } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonSummary.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonSummary.java new file mode 100644 index 000000000..10494595f --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/PersonSummary.java @@ -0,0 +1,29 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.jpa; + +import org.springframework.data.rest.core.config.Projection; + +/** + * @author Oliver Gierke + */ +@Projection(name = "excerpt", types = Person.class) +public interface PersonSummary { + + String getFirstName(); + + String getLastName(); +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java index 4b3dd2f43..d9e14c6c2 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java @@ -114,8 +114,8 @@ public class PersistentEntitySerializationTests { PersistentEntity persistentEntity = repositories.getPersistentEntity(Person.class); Person person = people.save(new Person("John", "Doe")); - PersistentEntityResource resource = PersistentEntityResource.wrap(persistentEntity, person, new Link( - "/person/" + person.getId())); + PersistentEntityResource resource = PersistentEntityResource.build(person, persistentEntity).// + withLink(new Link("/person/" + person.getId())).build(); StringWriter writer = new StringWriter(); mapper.writeValue(writer, resource); @@ -175,17 +175,18 @@ public class PersistentEntitySerializationTests { * @see DATAREST-250 */ @Test - @SuppressWarnings("unchecked") public void serializesEmbeddedReferencesCorrectly() throws Exception { User user = new User(); user.address = new Address(); user.address.street = "Street"; - PersistentEntityResource userResource = new PersistentEntityResource( - repositories.getPersistentEntity(User.class), user, new Link("/users/1")); + PersistentEntityResource userResource = PersistentEntityResource.// + build(user, repositories.getPersistentEntity(User.class)).// + withLink(new Link("/users/1")).// + build(); - PagedResources> persistentEntityResource = new PagedResources>( + PagedResources persistentEntityResource = new PagedResources( Arrays.asList(userResource), new PageMetadata(1, 0, 10)); String result = mapper.writeValueAsString(persistentEntityResource); @@ -205,12 +206,12 @@ public class PersistentEntitySerializationTests { order.add(new LineItem("first")); order.add(new LineItem("second")); - PersistentEntityResource orderResource = new PersistentEntityResource( - repositories.getPersistentEntity(Order.class), order); - orderResource.add(new Link("/orders/1")); + PersistentEntityResource orderResource = PersistentEntityResource.// + build(order, repositories.getPersistentEntity(Order.class)).// + withLink(new Link("/orders/1")).// + build(); - @SuppressWarnings("unchecked") - PagedResources> persistentEntityResource = new PagedResources>( + PagedResources persistentEntityResource = new PagedResources( Arrays.asList(orderResource), new PageMetadata(1, 0, 10)); String result = mapper.writeValueAsString(persistentEntityResource); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjectorUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjectorUnitTests.java index ec10beb02..abde640da 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjectorUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/PersistentEntityProjectorUnitTests.java @@ -25,6 +25,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; +import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.projection.ProjectionFactory; /** @@ -36,6 +37,7 @@ import org.springframework.data.rest.core.projection.ProjectionFactory; public class PersistentEntityProjectorUnitTests { @Mock ProjectionFactory factory; + @Mock ResourceMappings mappings; ProjectionDefinitionConfiguration configuration; @Before @@ -49,7 +51,7 @@ public class PersistentEntityProjectorUnitTests { @Test public void returnsObjectAsIfNoProjectionTypeFound() { - Projector projector = new PersistentEntityProjector(configuration, factory, "sample"); + Projector projector = new PersistentEntityProjector(configuration, factory, "sample", mappings); Object object = new Object(); assertThat(projector.project(object), is(object)); @@ -63,7 +65,7 @@ public class PersistentEntityProjectorUnitTests { configuration.addProjection(Sample.class, Object.class); - Projector projector = new PersistentEntityProjector(configuration, factory, "sample"); + Projector projector = new PersistentEntityProjector(configuration, factory, "sample", mappings); Object source = new Object(); projector.project(source); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java index 73fa429e1..587a599a6 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java @@ -15,11 +15,12 @@ */ package org.springframework.data.rest.webmvc.support; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.AbstractControllerIntegrationTests; import org.springframework.data.rest.webmvc.jpa.Book; @@ -45,7 +46,7 @@ public class RepositoryEntityLinksIntegrationTests extends AbstractControllerInt Link link = entityLinks.linkToSingleResource(Person.class, 1); - assertThat(link.getHref(), endsWith("/people/1")); + assertThat(link.getHref(), endsWith("/people/1{?projection}")); assertThat(link.getRel(), is("person")); } @@ -78,6 +79,19 @@ public class RepositoryEntityLinksIntegrationTests extends AbstractControllerInt public void usesCustomGeneratedBackendId() { Link link = entityLinks.linkToSingleResource(Book.class, 7L); - assertThat(link.getHref(), endsWith("/7-7-7-7-7-7-7")); + assertThat(link.expand().getHref(), endsWith("/7-7-7-7-7-7-7")); + } + + /** + * @see DATAREST-317 + */ + @Test + public void adaptsToExistingPageable() { + + Link link = entityLinks.linkToPagedResource(Person.class, new PageRequest(0, 10)); + + assertThat(link.isTemplated(), is(true)); + assertThat(link.getVariableNames(), hasSize(2)); + assertThat(link.getVariableNames(), hasItems("sort", "projection")); } }