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 12fda4136..a680c7622 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 @@ -54,6 +54,7 @@ import org.springframework.data.rest.webmvc.support.ETagDoesntMatchException; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; @@ -87,6 +88,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem MediaType.APPLICATION_JSON_VALUE); private static final String ACCEPT_HEADER = "Accept"; + private static final String LINK_HEADER = "Link"; private final RepositoryEntityLinks entityLinks; private final RepositoryRestConfiguration config; @@ -94,6 +96,18 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem private ApplicationEventPublisher publisher; + /** + * Creates a new {@link RepositoryEntityController} for the given {@link Repositories}, + * {@link RepositoryRestConfiguration}, {@link RepositoryEntityLinks}, {@link PagedResourcesAssembler}, + * {@link ConversionService} and {@link AuditableBeanWrapperFactory}. + * + * @param repositories must not be {@literal null}. + * @param config must not be {@literal null}. + * @param entityLinks must not be {@literal null}. + * @param assembler must not be {@literal null}. + * @param conversionService must not be {@literal null}. + * @param auditableBeanWrapperFactory must not be {@literal null}. + */ @Autowired public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config, RepositoryEntityLinks entityLinks, PagedResourcesAssembler assembler, @@ -143,8 +157,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem * @since 2.2 */ @RequestMapping(value = BASE_MAPPING, method = RequestMethod.HEAD) - public ResponseEntity headCollectionResource(RootResourceInformation resourceInformation) - throws HttpRequestMethodNotSupportedException { + public ResponseEntity headCollectionResource(RootResourceInformation resourceInformation, + DefaultedPageable pageable) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.HEAD, ResourceType.COLLECTION); @@ -154,7 +168,13 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem throw new ResourceNotFoundException(); } - return new ResponseEntity(HttpStatus.NO_CONTENT); + List links = getCollectionResourceLinks(resourceInformation, pageable); + links.add(0, getDefaultSelfLink()); + + HttpHeaders headers = new HttpHeaders(); + headers.add(LINK_HEADER, new Links(links).toString()); + + return new ResponseEntity(headers, HttpStatus.NO_CONTENT); } /** @@ -171,8 +191,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public Resources getCollectionResource(RootResourceInformation resourceInformation, DefaultedPageable pageable, - Sort sort, PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException, - HttpRequestMethodNotSupportedException { + Sort sort, PersistentEntityResourceAssembler assembler) + throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.COLLECTION); @@ -190,32 +210,40 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem results = invoker.invokeFindAll(sort); } + ResourceMetadata metadata = resourceInformation.getResourceMetadata(); + Link baseLink = entityLinks.linkToPagedResource(resourceInformation.getDomainType(), + pageable.isDefault() ? null : pageable.getPageable()); + + Resources result = toResources(results, assembler, metadata.getDomainType(), baseLink); + result.add(getCollectionResourceLinks(resourceInformation, pageable)); + return result; + } + + private List getCollectionResourceLinks(RootResourceInformation resourceInformation, + DefaultedPageable pageable) { + ResourceMetadata metadata = resourceInformation.getResourceMetadata(); SearchResourceMappings searchMappings = metadata.getSearchResourceMappings(); + List links = new ArrayList(); + links.add(new Link(ProfileController.getPath(this.config, metadata), ProfileResourceProcessor.PROFILE_REL)); + if (searchMappings.isExported()) { links.add(entityLinks.linkFor(metadata.getDomainType()).slash(searchMappings.getPath()) .withRel(searchMappings.getRel())); } - Link baseLink = entityLinks.linkToPagedResource(resourceInformation.getDomainType(), pageable.isDefault() ? null - : pageable.getPageable()); - - links.add(new Link(ProfileController.getPath(this.config, metadata), ProfileResourceProcessor.PROFILE_REL)); - - Resources result = toResources(results, assembler, metadata.getDomainType(), baseLink); - result.add(links); - return result; + return links; } @ResponseBody @SuppressWarnings({ "unchecked" }) - @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { - "application/x-spring-data-compact+json", "text/uri-list" }) + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, + produces = { "application/x-spring-data-compact+json", "text/uri-list" }) public Resources getCollectionResourceCompact(RootResourceInformation repoRequest, DefaultedPageable pageable, - Sort sort, PersistentEntityResourceAssembler assembler) throws ResourceNotFoundException, - HttpRequestMethodNotSupportedException { + Sort sort, PersistentEntityResourceAssembler assembler) + throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { Resources resources = getCollectionResource(repoRequest, pageable, sort, assembler); List links = new ArrayList(resources.getLinks()); @@ -244,8 +272,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST) public ResponseEntity postCollectionResource(RootResourceInformation resourceInformation, - PersistentEntityResource payload, PersistentEntityResourceAssembler assembler, @RequestHeader( - value = ACCEPT_HEADER, required = false) String acceptHeader) throws HttpRequestMethodNotSupportedException { + PersistentEntityResource payload, PersistentEntityResourceAssembler assembler, + @RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader) + throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.POST, ResourceType.COLLECTION); @@ -291,9 +320,12 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem throw new ResourceNotFoundException(); } - PersistentEntityResource resource = assembler.toResource(domainObject); + Links links = new Links(assembler.toResource(domainObject).getLinks()); - return new ResponseEntity(prepareHeaders(resource), HttpStatus.NO_CONTENT); + HttpHeaders headers = prepareHeaders(resourceInformation.getPersistentEntity(), domainObject); + headers.add(LINK_HEADER, links.toString()); + + return new ResponseEntity(headers, HttpStatus.NO_CONTENT); } /** @@ -361,7 +393,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem public ResponseEntity putItemResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler, ETag eTag, @RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader) - throws HttpRequestMethodNotSupportedException { + throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM); @@ -399,7 +431,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem public ResponseEntity patchItemResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler, ETag eTag, @RequestHeader(value = ACCEPT_HEADER, required = false) String acceptHeader) - throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { + throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/CommonWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/CommonWebTests.java index fdd354b3e..a051f7659 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/CommonWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/CommonWebTests.java @@ -17,18 +17,19 @@ package org.springframework.data.rest.webmvc; import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import static org.junit.Assume.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import net.minidev.json.JSONArray; + import java.util.List; import java.util.Map; -import net.minidev.json.JSONArray; - import org.junit.Test; - import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; @@ -205,7 +206,8 @@ public abstract class CommonWebTests extends AbstractWebIntegrationTests { mvc.perform(// get(profileLink.expand().getHref()).// - accept(ALPS_MEDIA_TYPE)).// + accept(ALPS_MEDIA_TYPE)) + .// andExpect(status().isOk()).// andExpect(content().contentType(ALPS_MEDIA_TYPE)); } @@ -219,4 +221,25 @@ public abstract class CommonWebTests extends AbstractWebIntegrationTests { mvc.perform(get("/index.html")).// andExpect(status().isNotFound()); } + + /** + * @see DATAREST-658 + */ + @Test + public void collectionResourcesExposeLinksAsHeadersForHeadRequest() throws Exception { + + for (String rel : expectedRootLinkRels()) { + + Link link = client.discoverUnique(rel); + + MockHttpServletResponse response = mvc.perform(head(link.expand().getHref()))// + .andExpect(status().isNoContent())// + .andReturn().getResponse(); + + Links links = Links.valueOf(response.getHeader("Link")); + + assertThat(links.hasLink(Link.REL_SELF), is(true)); + assertThat(links.hasLink("profile"), 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 48a3b2c3d..f04ec776d 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 @@ -34,6 +34,7 @@ import org.springframework.data.rest.webmvc.jpa.CreditCard; import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; import org.springframework.data.rest.webmvc.jpa.Order; import org.springframework.data.rest.webmvc.jpa.Person; +import org.springframework.data.rest.webmvc.support.DefaultedPageable; import org.springframework.data.rest.webmvc.support.ETag; import org.springframework.hateoas.Resource; import org.springframework.http.HttpEntity; @@ -92,8 +93,8 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll RootResourceInformation information = getResourceInformation(Order.class); - PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()), - entities.getPersistentEntity(Order.class)).build(); + PersistentEntityResource persistentEntityResource = PersistentEntityResource + .build(new Order(new Person()), entities.getPersistentEntity(Order.class)).build(); ResponseEntity entity = controller.putItemResource(information, persistentEntityResource, 1L, assembler, ETag.NO_ETAG, MediaType.APPLICATION_JSON_VALUE); @@ -106,7 +107,8 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll */ @Test public void exposesHeadForCollectionResourceIfExported() throws Exception { - ResponseEntity entity = controller.headCollectionResource(getResourceInformation(Person.class)); + ResponseEntity entity = controller.headCollectionResource(getResourceInformation(Person.class), + new DefaultedPageable(null, false)); assertThat(entity.getStatusCode(), is(HttpStatus.NO_CONTENT)); } @@ -115,7 +117,7 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll */ @Test(expected = ResourceNotFoundException.class) public void doesNotExposeHeadForCollectionResourceIfNotExported() throws Exception { - controller.headCollectionResource(getResourceInformation(CreditCard.class)); + controller.headCollectionResource(getResourceInformation(CreditCard.class), new DefaultedPageable(null, false)); } /** @@ -181,10 +183,11 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll List value = entity.getHeaders().get("Accept-Patch"); assertThat(value, hasSize(3)); - assertThat(value, hasItems(// - RestMediaTypes.JSON_PATCH_JSON.toString(), // - RestMediaTypes.MERGE_PATCH_JSON.toString(), // - MediaType.APPLICATION_JSON_VALUE)); + assertThat(value, + hasItems(// + RestMediaTypes.JSON_PATCH_JSON.toString(), // + RestMediaTypes.MERGE_PATCH_JSON.toString(), // + MediaType.APPLICATION_JSON_VALUE)); } /** @@ -196,12 +199,11 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll RootResourceInformation request = getResourceInformation(Order.class); Order order = request.getInvoker().invokeSave(new Order(new Person())); - PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()), - entities.getPersistentEntity(Order.class)).build(); + PersistentEntityResource persistentEntityResource = PersistentEntityResource + .build(new Order(new Person()), entities.getPersistentEntity(Order.class)).build(); - assertThat( - controller.putItemResource(request, persistentEntityResource, order.getId(), assembler, ETag.NO_ETAG, - MediaType.APPLICATION_JSON_VALUE).hasBody(), is(true)); + assertThat(controller.putItemResource(request, persistentEntityResource, order.getId(), assembler, ETag.NO_ETAG, + MediaType.APPLICATION_JSON_VALUE).hasBody(), is(true)); } /** @@ -211,12 +213,11 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll public void returnsBodyForCreatingPutIfAcceptHeaderPresentByDefault() throws HttpRequestMethodNotSupportedException { RootResourceInformation request = getResourceInformation(Order.class); - PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()), - entities.getPersistentEntity(Order.class)).build(); + PersistentEntityResource persistentEntityResource = PersistentEntityResource + .build(new Order(new Person()), entities.getPersistentEntity(Order.class)).build(); - assertThat( - controller.putItemResource(request, persistentEntityResource, 1L, assembler, ETag.NO_ETAG, - MediaType.APPLICATION_JSON_VALUE).hasBody(), is(true)); + assertThat(controller.putItemResource(request, persistentEntityResource, 1L, assembler, ETag.NO_ETAG, + MediaType.APPLICATION_JSON_VALUE).hasBody(), is(true)); } /** @@ -226,12 +227,12 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll public void returnsBodyForPostIfAcceptHeaderIsPresentByDefault() throws Exception { RootResourceInformation request = getResourceInformation(Order.class); - PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()), - entities.getPersistentEntity(Order.class)).build(); + PersistentEntityResource persistentEntityResource = PersistentEntityResource + .build(new Order(new Person()), entities.getPersistentEntity(Order.class)).build(); - assertThat( - controller.postCollectionResource(request, persistentEntityResource, assembler, - MediaType.APPLICATION_JSON_VALUE).hasBody(), is(true)); + assertThat(controller + .postCollectionResource(request, persistentEntityResource, assembler, MediaType.APPLICATION_JSON_VALUE) + .hasBody(), is(true)); } /** @@ -241,12 +242,13 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll public void doesNotReturnBodyForPostIfNoAcceptHeaderPresentByDefault() throws Exception { RootResourceInformation request = getResourceInformation(Order.class); - PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()), - entities.getPersistentEntity(Order.class)).build(); + PersistentEntityResource persistentEntityResource = PersistentEntityResource + .build(new Order(new Person()), entities.getPersistentEntity(Order.class)).build(); assertThat(controller.postCollectionResource(request, persistentEntityResource, assembler, null).hasBody(), is(false)); - assertThat(controller.postCollectionResource(request, persistentEntityResource, assembler, "").hasBody(), is(false)); + assertThat(controller.postCollectionResource(request, persistentEntityResource, assembler, "").hasBody(), + is(false)); } /** 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 e161380f2..35ca5ce37 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 @@ -22,6 +22,8 @@ import static org.springframework.http.HttpHeaders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import net.minidev.json.JSONArray; + import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; @@ -29,16 +31,14 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import net.minidev.json.JSONArray; - import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.CommonWebTests; import org.springframework.hateoas.Link; +import org.springframework.hateoas.Links; import org.springframework.hateoas.RelProvider; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; @@ -240,8 +240,8 @@ public class JpaWebTests extends CommonWebTests { Link peopleLink = client.discoverUnique("people"); - MockHttpServletResponse bilbo = postAndGet(peopleLink,// - "{ \"firstName\" : \"Bilbo\", \"lastName\" : \"Baggins\" }",// + MockHttpServletResponse bilbo = postAndGet(peopleLink, // + "{ \"firstName\" : \"Bilbo\", \"lastName\" : \"Baggins\" }", // MediaType.APPLICATION_JSON); Link bilboLink = client.assertHasLinkWithRel("self", bilbo); @@ -249,8 +249,8 @@ public class JpaWebTests extends CommonWebTests { assertThat((String) JsonPath.read(bilbo.getContentAsString(), "$.firstName"), equalTo("Bilbo")); assertThat((String) JsonPath.read(bilbo.getContentAsString(), "$.lastName"), equalTo("Baggins")); - MockHttpServletResponse frodo = putAndGet(bilboLink,// - "{ \"firstName\" : \"Frodo\" }",// + MockHttpServletResponse frodo = putAndGet(bilboLink, // + "{ \"firstName\" : \"Frodo\" }", // MediaType.APPLICATION_JSON); assertThat((String) JsonPath.read(frodo.getContentAsString(), "$.firstName"), equalTo("Frodo")); @@ -411,7 +411,8 @@ public class JpaWebTests extends CommonWebTests { String bilboWithFrodosLinks = createdPerson.getContentAsString().replace("Frodo", "Bilbo"); - MockHttpServletResponse overwrittenResponse = putAndGet(frodoLink, bilboWithFrodosLinks, MediaType.APPLICATION_JSON); + MockHttpServletResponse overwrittenResponse = putAndGet(frodoLink, bilboWithFrodosLinks, + MediaType.APPLICATION_JSON); client.assertHasLinkWithRel("self", overwrittenResponse); assertJsonPathEquals("$.firstName", "Bilbo", overwrittenResponse); @@ -588,15 +589,13 @@ public class JpaWebTests extends CommonWebTests { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(tacosLink.getHref()); String concurrencyTag = createdReceipt.getHeader("ETag"); - mvc.perform( - patch(builder.build().toUriString()).content("{ \"saleItem\" : \"SpringyBurritos\" }") - .contentType(MediaType.APPLICATION_JSON).header(IF_MATCH, concurrencyTag)).andExpect( - status().is2xxSuccessful()); + mvc.perform(patch(builder.build().toUriString()).content("{ \"saleItem\" : \"SpringyBurritos\" }") + .contentType(MediaType.APPLICATION_JSON).header(IF_MATCH, concurrencyTag)) + .andExpect(status().is2xxSuccessful()); - mvc.perform( - patch(builder.build().toUriString()).content("{ \"saleItem\" : \"SpringyTequila\" }") - .contentType(MediaType.APPLICATION_JSON).header(IF_MATCH, "\"falseETag\"")).andExpect( - status().isPreconditionFailed()); + mvc.perform(patch(builder.build().toUriString()).content("{ \"saleItem\" : \"SpringyTequila\" }") + .contentType(MediaType.APPLICATION_JSON).header(IF_MATCH, "\"falseETag\"")) + .andExpect(status().isPreconditionFailed()); } /** @@ -637,6 +636,24 @@ public class JpaWebTests extends CommonWebTests { } } + /** + * @see DATAREST-658 + */ + @Test + public void returnsLinkHeadersForHeadRequestToItemResource() throws Exception { + + MockHttpServletResponse response = client.request(client.discoverUnique("people")); + String personHref = JsonPath.read(response.getContentAsString(), "$._embedded.people[0]._links.self.href"); + + response = mvc.perform(head(personHref))// + .andExpect(status().isNoContent())// + .andReturn().getResponse(); + + Links links = Links.valueOf(response.getHeader("Link")); + assertThat(links.hasLink("self"), is(true)); + assertThat(links.hasLink("person"), is(true)); + } + private List preparePersonResources(Person primary, Person... persons) throws Exception { Link peopleLink = client.discoverUnique("people");