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 551a18374..41295d8f9 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 @@ -101,6 +101,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 getCollectionResource(final RootResourceInformation resourceInformation, @@ -181,6 +214,25 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem return createAndReturn(payload.getContent(), resourceInformation.getInvoker(), assembler); } + /** + * 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. * @@ -194,15 +246,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @BackendId Serializable id, PersistentEntityResourceAssembler assembler) 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); @@ -283,14 +327,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)); @@ -366,4 +402,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 7cbd79c88..fd856538e 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; @@ -89,7 +89,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 @@ -98,11 +113,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 ccccb504e..68837534f 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 @@ public class RootResourceInformation { if (invoker.exposesFindAll()) { methods.add(HttpMethod.GET); + methods.add(HttpMethod.HEAD); } if (invoker.exposesSave()) { @@ -111,6 +112,7 @@ public class RootResourceInformation { if (invoker.exposesFindOne()) { methods.add(HttpMethod.GET); + methods.add(HttpMethod.HEAD); } if (invoker.exposesSave()) { @@ -147,11 +149,16 @@ public 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 11caef5c0..8501042b5 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.mapping.context.PersistentEntities; 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; @@ -85,4 +87,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 32974e327..e01426760 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 @@ -95,4 +95,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 2f1e9ce57..bc993bac3 100644 --- a/src/docbkx/repository-resources.xml +++ b/src/docbkx/repository-resources.xml @@ -22,7 +22,7 @@ at /orders. The path is derived from the uncapitalized, pluralized, simple class name of the domain class being managed. It also exposes an item resource for each of the items managed by the repository - under the URI template /orders/{id}. + under the URI template /orders/{id}. By default the HTTP methods to interact with these resources map to the according methods of CrudRepository. @@ -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> @@ -379,7 +391,7 @@ each of the associations the item resource has. The name and path of the of the resource defaults to the name of the association property and can be customized using @RestResource on the - association property. + association property.
Supported HTTP methods @@ -408,7 +420,7 @@ PUT Binds the resource pointed to by the given URI(s) to the - resource. This + resource. This Custom status codes @@ -514,6 +526,13 @@ parameters.
+ +
+ <code>HEAD</code> + + Returns whether the search resource is available. A 404 return + code indicates no query method resources available at all. +
@@ -573,6 +592,12 @@ + +
+ <code>HEAD</code> + + Returns whether a query method resource is available. +