DATAREST-333 - Added controller mappings for OPTIONS requests.

The root resource, collection and item resources as well as the search and query method resources now expose a handler method to handle OPTIONS requests and return a response with the Allow header set to the HTTP methods appropriate to the resource requested.

Added some additional methods for HEAD requests and a few integration tests for functionality that previously existed.

Related ticket: DATAREST-330.
This commit is contained in:
Oliver Gierke
2014-06-26 09:43:37 +02:00
parent 8618b7df8e
commit e70285331c
10 changed files with 298 additions and 13 deletions

View File

@@ -45,6 +45,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.Assert;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@@ -61,7 +62,14 @@ class AbstractRepositoryRestController implements MessageSourceAware {
private final PagedResourcesAssembler<Object> pagedResourcesAssembler;
private MessageSourceAccessor messageSourceAccessor;
/**
* Creates a new {@link AbstractRepositoryRestController} for the given {@link PagedResourcesAssembler}.
*
* @param pagedResourcesAssembler must not be {@literal null}.
*/
public AbstractRepositoryRestController(PagedResourcesAssembler<Object> pagedResourcesAssembler) {
Assert.notNull(pagedResourcesAssembler, "PagedResourcesAssembler must not be null!");
this.pagedResourcesAssembler = pagedResourcesAssembler;
}

View File

@@ -15,46 +15,94 @@
*/
package org.springframework.data.rest.webmvc;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
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.web.PagedResourcesAssembler;
import org.springframework.hateoas.EntityLinks;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Controller for the root resource exposing links to the repository resources.
*
* @author Jon Brisbin
* @author Oliver Gierke
*/
@RepositoryRestController
@RequestMapping("/")
public class RepositoryController extends AbstractRepositoryRestController {
private final Repositories repositories;
private final EntityLinks entityLinks;
private final ResourceMappings mappings;
/**
* Creates a new {@link RepositoryController} for the given {@link PagedResourcesAssembler}, {@link Repositories},
* {@link EntityLinks} and {@link ResourceMappings}.
*
* @param assembler must not be {@literal null}.
* @param repositories must not be {@literal null}.
* @param entityLinks must not be {@literal null}.
* @param mappings must not be {@literal null}.
*/
@Autowired
public RepositoryController(PagedResourcesAssembler<Object> assembler, Repositories repositories,
EntityLinks entityLinks, ResourceMappings mappings) {
super(assembler);
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(entityLinks, "EntityLinks must not be null!");
Assert.notNull(mappings, "ResourceMappings must not be null!");
this.repositories = repositories;
this.entityLinks = entityLinks;
this.mappings = mappings;
}
/**
* <code>OPTIONS /</code>.
*
* @return
* @since 2.2
*/
@RequestMapping(method = RequestMethod.OPTIONS)
public HttpEntity<?> optionsForRepositories() {
HttpHeaders headers = new HttpHeaders();
headers.setAllow(Collections.singleton(HttpMethod.GET));
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
/**
* <code>HEAD /</code>
*
* @return
* @since 2.2
*/
@RequestMapping(method = RequestMethod.HEAD)
public ResponseEntity<?> headForRepositories() {
return new ResponseEntity<Object>(HttpStatus.NO_CONTENT);
}
/**
* Lists all repositories exported by creating a link list pointing to resources exposing the repositories.
*
* @return
*/
@ResponseBody
@RequestMapping(value = "/", method = RequestMethod.GET)
public RepositoryLinksResource listRepositories() {
@RequestMapping(method = RequestMethod.GET)
public HttpEntity<RepositoryLinksResource> listRepositories() {
RepositoryLinksResource resource = new RepositoryLinksResource();
@@ -66,6 +114,6 @@ public class RepositoryController extends AbstractRepositoryRestController {
}
}
return resource;
return new ResponseEntity<RepositoryLinksResource>(resource, HttpStatus.OK);
}
}

View File

@@ -101,12 +101,29 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
this.publisher = publisher;
}
/**
* <code>OPTIONS /{repository}</code>.
*
* @param information
* @return
* @since 2.2
*/
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.OPTIONS)
public ResponseEntity<?> optionsForCollectionResource(RootResourceInformation information) {
HttpHeaders headers = new HttpHeaders();
headers.setAllow(information.getSupportedMethods(ResourceType.COLLECTION));
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
/**
* <code>HEAD /{repository}</code>
*
* @param resourceInformation
* @return
* @throws HttpRequestMethodNotSupportedException
* @since 2.2
*/
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.HEAD)
public ResponseEntity<?> headCollectionResource(RootResourceInformation resourceInformation)
@@ -214,6 +231,22 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
return createAndReturn(payload.getContent(), resourceInformation.getInvoker(), assembler);
}
/**
* <code>OPTIONS /{repository}/{id}<code>
*
* @param information
* @return
* @since 2.2
*/
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.OPTIONS)
public ResponseEntity<?> optionsForItemResource(RootResourceInformation information) {
HttpHeaders headers = new HttpHeaders();
headers.setAllow(information.getSupportedMethods(ResourceType.ITEM));
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
/**
* <code>HEAD /{repsoitory}/{id}</code>
*
@@ -221,9 +254,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
* @param id
* @return
* @throws HttpRequestMethodNotSupportedException
* @since 2.2
*/
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.HEAD)
public ResponseEntity<?> headItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id)
public ResponseEntity<?> headForItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id)
throws HttpRequestMethodNotSupportedException {
if (getItemResource(resourceInformation, id) != null) {

View File

@@ -20,6 +20,7 @@ import static org.springframework.data.rest.webmvc.ControllerUtils.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -38,6 +39,8 @@ import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
@@ -88,6 +91,24 @@ class RepositorySearchController extends AbstractRepositoryRestController {
this.assembler = assembler;
}
/**
* <code>OPTIONS /{repository}/search</code>.
*
* @param resourceInformation
* @return
* @since 2.2
*/
@RequestMapping(value = BASE_MAPPING, method = RequestMethod.OPTIONS)
public HttpEntity<?> optionsForSearches(RootResourceInformation resourceInformation) {
verifySearchesExposed(resourceInformation);
HttpHeaders headers = new HttpHeaders();
headers.setAllow(Collections.singleton(HttpMethod.GET));
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
/**
* <code>HEAD /{repository}/search</code> - Checks whether the search resource is present.
*
@@ -187,12 +208,32 @@ class RepositorySearchController extends AbstractRepositoryRestController {
return new Resources<Resource<?>>(EMPTY_RESOURCE_LIST, links);
}
/**
* <code>OPTIONS /{repository}/search/{search}</code>.
*
* @param information
* @param search
* @return
* @since 2.2
*/
@RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.OPTIONS)
public ResponseEntity<Object> optionsForSearch(RootResourceInformation information, @PathVariable String search) {
checkExecutability(information, search);
HttpHeaders headers = new HttpHeaders();
headers.setAllow(Collections.singleton(HttpMethod.GET));
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
/**
* Handles a {@code HEAD} request for individual searches.
*
* @param information
* @param search
* @return
* @since 2.2
*/
@RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.HEAD)
public ResponseEntity<Object> headForSearch(RootResourceInformation information, @PathVariable String search) {

View File

@@ -80,7 +80,7 @@ public class RootResourceInformation {
* @param resourcType must not be {@literal null}.
* @return
*/
public Collection<HttpMethod> getSupportedMethods(ResourceType resourcType) {
public Set<HttpMethod> getSupportedMethods(ResourceType resourcType) {
Assert.notNull(resourcType, "Resource type must not be null!");
@@ -89,6 +89,7 @@ public class RootResourceInformation {
}
Set<HttpMethod> methods = new HashSet<HttpMethod>();
methods.add(HttpMethod.OPTIONS);
switch (resourcType) {
case COLLECTION:

View File

@@ -0,0 +1,73 @@
/*
* 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 static org.springframework.data.rest.webmvc.WebTestUtils.*;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;
/**
* Integration tests for {@link RepositoryController}.
*
* @author Oliver Gierke
*/
@ContextConfiguration(classes = JpaRepositoryConfig.class)
@Transactional
public class RepositoryControllerIntegrationTests extends AbstractControllerIntegrationTests {
@Autowired RepositoryController controller;
/**
* @see DATAREST-333
*/
@Test
public void rootResourceExposesGetOnly() {
HttpEntity<?> response = controller.optionsForRepositories();
assertAllowHeaders(response, HttpMethod.GET);
}
/**
* @see DATAREST-333, DATAREST-330
*/
@Test
public void headRequestReturnsNoContent() {
assertThat(controller.headForRepositories().getStatusCode(), is(HttpStatus.NO_CONTENT));
}
@Test
public void exposesLinksToRepositories() {
RepositoryLinksResource resource = controller.listRepositories().getBody();
assertThat(resource.getLinks(), hasSize(5));
assertThat(resource.hasLink("people"), is(true));
assertThat(resource.hasLink("orders"), is(true));
assertThat(resource.hasLink("addresses"), is(true));
assertThat(resource.hasLink("books"), is(true));
assertThat(resource.hasLink("authors"), is(true));
}
}

View File

@@ -17,6 +17,8 @@ package org.springframework.data.rest.webmvc;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.rest.webmvc.WebTestUtils.*;
import static org.springframework.http.HttpMethod.*;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -28,6 +30,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.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
@@ -113,7 +116,7 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
Address address = repository.save(new Address());
ResponseEntity<?> entity = controller.headItemResource(getResourceInformation(Address.class), address.id);
ResponseEntity<?> entity = controller.headForItemResource(getResourceInformation(Address.class), address.id);
assertThat(entity.getStatusCode(), is(HttpStatus.NO_CONTENT));
}
@@ -122,6 +125,36 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll
*/
@Test(expected = ResourceNotFoundException.class)
public void doesNotExposeHeadForItemResourceIfNotExisting() throws Exception {
controller.headItemResource(getResourceInformation(CreditCard.class), 1L);
controller.headForItemResource(getResourceInformation(CreditCard.class), 1L);
}
/**
* @see DATAREST-333
*/
@Test
public void doesNotExposeMethodsForOptionsIfNotHttpMethodsSupportedForCollectionResource() {
HttpEntity<?> response = controller.optionsForCollectionResource(getResourceInformation(Address.class));
assertAllowHeaders(response, OPTIONS);
}
/**
* @see DATAREST-333
*/
@Test
public void exposesSupportedHttpMethodsInAllowHeaderForOptionsRequestToCollectionResource() {
HttpEntity<?> response = controller.optionsForCollectionResource(getResourceInformation(Person.class));
assertAllowHeaders(response, GET, POST, HEAD, OPTIONS);
}
/**
* @see DATAREST-333
*/
@Test
public void exposesSupportedHttpMethodsInAllowHeaderForOptionsRequestToItemResource() {
HttpEntity<?> response = controller.optionsForItemResource(getResourceInformation(Person.class));
assertAllowHeaders(response, GET, PUT, PATCH, DELETE, HEAD, OPTIONS);
}
}

View File

@@ -15,14 +15,16 @@
*/
package org.springframework.data.rest.webmvc;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.rest.webmvc.WebTestUtils.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.ResourceTester.HasSelfLink;
import org.springframework.data.rest.webmvc.jpa.Address;
import org.springframework.data.rest.webmvc.jpa.CreditCard;
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
import org.springframework.data.rest.webmvc.jpa.Order;
@@ -30,6 +32,8 @@ import org.springframework.data.rest.webmvc.jpa.Person;
import org.springframework.data.rest.webmvc.jpa.TestDataPopulator;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;
@@ -135,4 +139,32 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll
public void doesNotExposeHeadForInvalidQueryMethodResource() {
controller.headForSearch(getResourceInformation(Person.class), "foobar");
}
/**
* @see DATAREST-333
*/
@Test
public void searchResourceSupportsGetOnly() {
assertAllowHeaders(controller.optionsForSearches(getResourceInformation(Person.class)), HttpMethod.GET);
}
/**
* @see DATAREST-333
*/
@Test(expected = ResourceNotFoundException.class)
public void returns404ForOptionsForRepositoryWithoutSearches() {
controller.optionsForSearches(getResourceInformation(Address.class));
}
/**
* @see DATAREST-333
*/
@Test
public void queryMethodResourceSupportsGetOnly() {
RootResourceInformation resourceInformation = getResourceInformation(Person.class);
HttpEntity<Object> response = controller.optionsForSearch(resourceInformation, "firstname");
assertAllowHeaders(response, HttpMethod.GET);
}
}

View File

@@ -21,6 +21,7 @@ import static org.mockito.Mockito.*;
import static org.springframework.data.rest.webmvc.ResourceType.*;
import static org.springframework.http.HttpMethod.*;
import org.atteo.evo.inflector.English;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -57,15 +58,15 @@ public class RootResourceInformationUnitTests {
}
/**
* @see DATAREST-217
* @see DATAREST-217, DATAREST-330
*/
@Test
public void defaultsSupportedHttpMethodsForItemResource() {
assertThat(information.getSupportedMethods(ResourceType.ITEM), hasItems(GET, PUT, PATCH, DELETE));
assertThat(information.getSupportedMethods(ResourceType.ITEM), hasItems(GET, PUT, PATCH, DELETE, OPTIONS));
assertThat(information.getSupportedMethods(ResourceType.ITEM), not(hasItems(POST)));
assertThat(information.getSupportedMethods(COLLECTION), hasItems(GET, POST));
assertThat(information.getSupportedMethods(COLLECTION), hasItems(GET, POST, OPTIONS));
assertThat(information.getSupportedMethods(COLLECTION), not(hasItems(PUT, PATCH, DELETE)));
}

View File

@@ -15,6 +15,12 @@
*/
package org.springframework.data.rest.webmvc;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@@ -35,4 +41,12 @@ public class WebTestUtils {
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(requestAttributes);
}
public static void assertAllowHeaders(HttpEntity<?> response, HttpMethod... methods) {
HttpHeaders headers = response.getHeaders();
assertThat(headers.getAllow(), hasSize(methods.length));
assertThat(headers.getAllow(), hasItems(methods));
}
}