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")); } }