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 035d16363..a288d3d1b 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 @@ -104,6 +104,39 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem this.publisher = publisher; } + /** + * HEAD /{repository} + * + * @param resourceInformation + * @return + * @throws HttpRequestMethodNotSupportedException + */ + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.HEAD) + public ResponseEntity headCollectionResource(RootResourceInformation resourceInformation) + throws HttpRequestMethodNotSupportedException { + + resourceInformation.verifySupportedMethod(HttpMethod.HEAD, ResourceType.COLLECTION); + + RepositoryInvoker invoker = resourceInformation.getInvoker(); + + if (null == invoker) { + throw new ResourceNotFoundException(); + } + + return new ResponseEntity(HttpStatus.NO_CONTENT); + } + + /** + * GET /{repository} - Returns the collection resource (paged or unpaged). + * + * @param resourceInformation + * @param pageable + * @param sort + * @param assembler + * @return + * @throws ResourceNotFoundException + * @throws HttpRequestMethodNotSupportedException + */ @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public Resources listEntities(final RootResourceInformation resourceInformation, Pageable pageable, Sort sort) @@ -178,6 +211,25 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem return createAndReturn(payload.getContent(), resourceInformation.getInvoker()); } + /** + * HEAD /{repsoitory}/{id} + * + * @param resourceInformation + * @param id + * @return + * @throws HttpRequestMethodNotSupportedException + */ + @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.HEAD) + public ResponseEntity headItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id) + throws HttpRequestMethodNotSupportedException { + + if (getItemResource(resourceInformation, id) != null) { + return new ResponseEntity(HttpStatus.NO_CONTENT); + } + + throw new ResourceNotFoundException(); + } + /** * GET /{repository}/{id} - Returns a single entity. * @@ -190,15 +242,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem public ResponseEntity> getSingleEntity(RootResourceInformation resourceInformation, @BackendId Serializable id) throws HttpRequestMethodNotSupportedException { - resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.ITEM); - - RepositoryInvoker repoMethodInvoker = resourceInformation.getInvoker(); - - if (!repoMethodInvoker.exposesFindOne()) { - return new ResponseEntity>(HttpStatus.NOT_FOUND); - } - - Object domainObj = repoMethodInvoker.invokeFindOne(id); + Object domainObj = getItemResource(resourceInformation, id); if (domainObj == null) { return new ResponseEntity>(HttpStatus.NOT_FOUND); @@ -279,14 +323,6 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem resourceInformation.verifySupportedMethod(HttpMethod.DELETE, ResourceType.ITEM); RepositoryInvoker invoker = resourceInformation.getInvoker(); - - // TODO: re-enable not exposing delete method if hidden - - // ResourceMapping methodMapping = repoRequest.getRepositoryResourceMapping().getResourceMappingFor("delete"); - // if (null != methodMapping && !methodMapping.isExported()) { - // throw new HttpRequestMethodNotSupportedException("DELETE"); - // } - Object domainObj = invoker.invokeFindOne(id); publisher.publishEvent(new BeforeDeleteEvent(domainObj)); @@ -362,4 +398,27 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem String selfLink = assembler.getSelfLinkFor(source).getHref(); headers.setLocation(new UriTemplate(selfLink).expand()); } + + /** + * Returns the object backing the item resource for the given {@link RootResourceInformation} and id. + * + * @param resourceInformation + * @param id + * @return + * @throws HttpRequestMethodNotSupportedException + * @throws {@link ResourceNotFoundException} + */ + private Object getItemResource(RootResourceInformation resourceInformation, Serializable id) + throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { + + resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.ITEM); + + RepositoryInvoker repoMethodInvoker = resourceInformation.getInvoker(); + + if (!repoMethodInvoker.exposesFindOne()) { + throw new ResourceNotFoundException(); + } + + return repoMethodInvoker.invokeFindOne(id); + } } 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 da8e01684..12eaee44a 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 @@ -28,7 +28,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.rest.core.invoke.RepositoryInvoker; import org.springframework.data.rest.core.mapping.MethodResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMappings; -import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.EntityLinks; @@ -38,6 +37,7 @@ import org.springframework.hateoas.Links; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; @@ -90,7 +90,22 @@ class RepositorySearchController extends AbstractRepositoryRestController { } /** - * Exposes links to the individual search resources exposed by the backing repository. + * HEAD /{repository}/search - Checks whether the search resource is present. + * + * @param resourceInformation + * @return + */ + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.HEAD) + public HttpEntity headForSearches(RootResourceInformation resourceInformation) { + + verifySearchesExposed(resourceInformation); + + return new ResponseEntity(HttpStatus.NO_CONTENT); + } + + /** + * GET /{repository}/search - Exposes links to the individual search resources exposed by the backing + * repository. * * @param resourceInformation * @return @@ -99,11 +114,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public ResourceSupport listSearches(RootResourceInformation resourceInformation) { - SearchResourceMappings resourceMappings = resourceInformation.getSearchMappings(); - - if (!resourceMappings.isExported()) { - throw new ResourceNotFoundException(); - } + verifySearchesExposed(resourceInformation); Links queryMethodLinks = getSearchLinks(resourceInformation.getDomainType()); @@ -176,6 +187,20 @@ class RepositorySearchController extends AbstractRepositoryRestController { return new Resources>(EMPTY_RESOURCE_LIST, links); } + /** + * Handles a {@code HEAD} request for individual searches. + * + * @param information + * @param search + * @return + */ + @RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.HEAD) + public ResponseEntity headForSearch(RootResourceInformation information, @PathVariable String search) { + + checkExecutability(information, search); + return new ResponseEntity(HttpStatus.NO_CONTENT); + } + /** * Checks that the given request is actually executable. Will reject execution if we don't find a search with the * given name. @@ -186,12 +211,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { */ private Method checkExecutability(RootResourceInformation resourceInformation, String searchName) { - ResourceMetadata metadata = resourceInformation.getResourceMetadata(); - SearchResourceMappings searchMapping = metadata.getSearchResourceMappings(); - - if (!searchMapping.isExported()) { - throw new ResourceNotFoundException(); - } + SearchResourceMappings searchMapping = verifySearchesExposed(resourceInformation); Method method = searchMapping.getMappedMethod(searchName); @@ -260,4 +280,20 @@ class RepositorySearchController extends AbstractRepositoryRestController { String parameterString = StringUtils.collectionToCommaDelimitedString(parameters); return parameters.isEmpty() ? "" : String.format(PARAMETER_NAME_TEMPALTE_PATTERN, parameterString); } + + /** + * Verifies that the given {@link RootResourceInformation} has searches exposed. + * + * @param resourceInformation + */ + private SearchResourceMappings verifySearchesExposed(RootResourceInformation resourceInformation) { + + SearchResourceMappings resourceMappings = resourceInformation.getSearchMappings(); + + if (!resourceMappings.isExported()) { + throw new ResourceNotFoundException(); + } + + return resourceMappings; + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java index 2d8174746..3b0d19512 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java @@ -95,6 +95,7 @@ class RootResourceInformation { if (invoker.exposesFindAll()) { methods.add(HttpMethod.GET); + methods.add(HttpMethod.HEAD); } if (invoker.exposesSave()) { @@ -111,6 +112,7 @@ class RootResourceInformation { if (invoker.exposesFindOne()) { methods.add(HttpMethod.GET); + methods.add(HttpMethod.HEAD); } if (invoker.exposesSave()) { @@ -147,11 +149,16 @@ class RootResourceInformation { * * @param httpMethod must not be {@literal null}. * @param resourceType must not be {@literal null}. + * @throws ResourceNotFoundException if the repository is not exported at all. * @throws HttpRequestMethodNotSupportedException if the {@link ResourceType} does not support the given * {@link HttpMethod}. Will contain all supported methods as indicators for clients. */ public void verifySupportedMethod(HttpMethod httpMethod, ResourceType resourceType) - throws HttpRequestMethodNotSupportedException { + throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { + + if (!resourceMetadata.isExported()) { + throw new ResourceNotFoundException(); + } Assert.notNull(httpMethod, "HTTP method must not be null!"); Assert.notNull(resourceType, "Resource type must not be null!"); 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 ed5904898..c4ce2a4f3 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 @@ -24,9 +24,11 @@ import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.jpa.Address; import org.springframework.data.rest.webmvc.jpa.AddressRepository; +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.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ContextConfiguration; import org.springframework.transaction.annotation.Transactional; @@ -83,4 +85,41 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll assertThat(entity.getHeaders().getLocation().toString(), not(endsWith("{?projection}"))); } + + /** + * @see DATAREST-330 + */ + @Test + public void exposesHeadForCollectionResourceIfExported() throws Exception { + ResponseEntity entity = controller.headCollectionResource(getResourceInformation(Person.class)); + assertThat(entity.getStatusCode(), is(HttpStatus.NO_CONTENT)); + } + + /** + * @see DATAREST-330 + */ + @Test(expected = ResourceNotFoundException.class) + public void doesNotExposeHeadForCollectionResourceIfNotExported() throws Exception { + controller.headCollectionResource(getResourceInformation(CreditCard.class)); + } + + /** + * @see DATAREST-330 + */ + @Test + public void exposesHeadForItemResourceIfExported() throws Exception { + + Address address = repository.save(new Address()); + + ResponseEntity entity = controller.headItemResource(getResourceInformation(Address.class), address.id); + assertThat(entity.getStatusCode(), is(HttpStatus.NO_CONTENT)); + } + + /** + * @see DATAREST-330 + */ + @Test(expected = ResourceNotFoundException.class) + public void doesNotExposeHeadForItemResourceIfNotExisting() throws Exception { + controller.headItemResource(getResourceInformation(CreditCard.class), 1L); + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java index 8eb4e5444..d71aada45 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java @@ -94,4 +94,44 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll ResourceMetadata metadata = getMetadata(Person.class); tester.withContentResource(new HasSelfLink(BASE.slash(metadata.getPath()).slash("{id}"))); } + + /** + * @see DATAREST-330 + */ + @Test(expected = ResourceNotFoundException.class) + public void doesNotExposeHeadForSearchResourceIfResourceDoesnHaveSearches() { + controller.headForSearches(getResourceInformation(Order.class)); + } + + /** + * @see DATAREST-330 + */ + @Test(expected = ResourceNotFoundException.class) + public void exposesHeadForSearchResourceIfResourceIsNotExposed() { + controller.headForSearches(getResourceInformation(CreditCard.class)); + } + + /** + * @see DATAREST-330 + */ + @Test + public void exposesHeadForSearchResourceIfResourceIsExposed() { + controller.headForSearches(getResourceInformation(Person.class)); + } + + /** + * @see DATAREST-330 + */ + @Test + public void exposesHeadForExistingQueryMethodResource() { + controller.headForSearch(getResourceInformation(Person.class), "findByCreatedUsingISO8601Date"); + } + + /** + * @see DATAREST-330 + */ + @Test(expected = ResourceNotFoundException.class) + public void doesNotExposeHeadForInvalidQueryMethodResource() { + controller.headForSearch(getResourceInformation(Person.class), "foobar"); + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RootResourceInformationUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RootResourceInformationUnitTests.java index cd7819824..0ad386b5f 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RootResourceInformationUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RootResourceInformationUnitTests.java @@ -32,6 +32,7 @@ import org.mockito.stubbing.Answer; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.rest.core.invoke.RepositoryInvoker; import org.springframework.data.rest.core.mapping.ResourceMetadata; +import org.springframework.web.HttpRequestMethodNotSupportedException; /** * Unit tests for {@link RootResourceInformation}. @@ -99,6 +100,56 @@ public class RootResourceInformationUnitTests { assertThat(information.supports(POST, ITEM), is(false)); } + /** + * @see DATAREST-330 + */ + @Test + public void supportsHeadIfFindAllIsExposed() { + + when(invoker.exposesFindAll()).thenReturn(true); + assertThat(information.supports(HEAD, COLLECTION), is(true)); + } + + /** + * @see DATAREST-330 + */ + @Test + public void doesNotSupportHeadIfFindAllIsNotExposed() { + + when(invoker.exposesFindAll()).thenReturn(false); + assertThat(information.supports(HEAD, COLLECTION), is(false)); + } + + /** + * @see DATAREST-330 + */ + @Test + public void supportsHeadIfFindOneIsExposed() { + + when(invoker.exposesFindOne()).thenReturn(true); + assertThat(information.supports(HEAD, ITEM), is(true)); + } + + /** + * @see DATAREST-330 + */ + @Test + public void doesNotSupportHeadIfFindOneIsNotExposed() { + + when(invoker.exposesFindOne()).thenReturn(false); + assertThat(information.supports(HEAD, ITEM), is(false)); + } + + /** + * @see DATAREST-330 + */ + @Test(expected = ResourceNotFoundException.class) + public void throwsExceptionOnVerificationIfResourceIsNotExported() throws HttpRequestMethodNotSupportedException { + + when(metadata.isExported()).thenReturn(false); + information.verifySupportedMethod(HEAD, COLLECTION); + } + /** * Helper class to default boolean methods to return {@literal true} instead of {@literal false} by default. * diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Address.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Address.java index 086ce4563..b4ac3fb21 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Address.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/Address.java @@ -25,5 +25,5 @@ import javax.persistence.Id; @Entity public class Address { - @Id @GeneratedValue Long id; + public @Id @GeneratedValue Long id; } diff --git a/src/docbkx/repository-resources.xml b/src/docbkx/repository-resources.xml index 48443a7d3..12497e036 100644 --- a/src/docbkx/repository-resources.xml +++ b/src/docbkx/repository-resources.xml @@ -21,7 +21,7 @@ For this repository, Spring Data REST exposes a collection resource at /orders. The path is derived from the uncapitalized, pluralized, simple class name of the domain class being managed. It also - exposes a an item resource for each of the items managed by te repository + exposes an item resource for each of the items managed by the repository under the URI template /orders/{id}. By default the HTTP methods to interact with these resources map to @@ -191,6 +191,12 @@ +
+ <code>HEAD</code> + + Returns whether the collection resource is available. +
+
<code>POST</code> @@ -283,6 +289,12 @@
+
+ <code>HEAD</code> + + Returns whether the item resource is available. +
+
<code>PUT</code> @@ -513,6 +525,12 @@ pointing to it will be a URI template containing the pagination parameters. +
+
+ <code>HEAD</code> + + Returns whether the search resource is available. A 404 return + code indicates no query method resources available at all.
@@ -572,6 +590,11 @@ + +
+ <code>HEAD</code> + + Returns whether a query method resource is available.