Skip to content

Commit

Permalink
DATAREST-333 - Added controller mappings for OPTIONS requests.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odrotbohm committed Jun 26, 2014
1 parent 8618b7d commit e702853
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
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;
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("/")

This comment has been minimized.

Copy link
@ptahchiev

ptahchiev Jun 28, 2014

This line breaks my setup. I have my rest configuration configured in a separate jar like this:

    /* Spring REST Delegating Dispatcher Servlet */
    Servlet restDispatcherServlet = new RepositoryRestDispatcherServlet(webCtx);
    ServletRegistration.Dynamic restDispatcherServletReg = servletContext.addServlet("restDispatcherServlet", restDispatcherServlet);
    restDispatcherServletReg.setLoadOnStartup(1);
    restDispatcherServletReg.addMapping("/rest/*");

and also my storefront war (holds the rest jar in WEB-INF/lib) configured like this:

    final Servlet dispatcherServlet = new DispatcherServlet(webCtx);
    final ServletRegistration.Dynamic dispatcherServletReg = servletContext.addServlet("dispatcherServlet", dispatcherServlet);
    dispatcherServletReg.setInitParameter("contextConfigLocation", "");
    dispatcherServletReg.setLoadOnStartup(1);
    dispatcherServletReg.addMapping("/");

and now that I try to access the http://localhost:8111/storefront/ I get an exception that there are multiple controllers mapped to / :

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: Ambiguous handler methods mapped for HTTP
path 'http://localhost:8111/storefront/': {public java.lang.String com.xxxx.storefront.controllers.pages.HomePageController.home(org.springframework.ui.Model,java.lang
.String,javax.servlet.http.HttpServletRequest), public org.springframework.http.HttpEntity org.springframework.data.rest.webmvc.RepositoryController.listRepositories()}
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:973)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:687)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:711)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1644)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:118)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
...

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();

Expand All @@ -66,6 +114,6 @@ public RepositoryLinksResource listRepositories() {
}
}

return resource;
return new ResponseEntity<RepositoryLinksResource>(resource, HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,29 @@ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
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)
Expand Down Expand Up @@ -214,16 +231,33 @@ public ResponseEntity<ResourceSupport> postCollectionResource(RootResourceInform
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>
*
* @param resourceInformation
* @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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;

Expand All @@ -38,6 +39,8 @@
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;
Expand Down Expand Up @@ -88,6 +91,24 @@ public RepositorySearchController(PagedResourcesAssembler<Object> assembler, Ent
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.
*
Expand Down Expand Up @@ -187,12 +208,32 @@ public ResourceSupport executeSearchCompact(RootResourceInformation resourceInfo
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public RepositoryInvoker getInvoker() {
* @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!");

Expand All @@ -89,6 +89,7 @@ public Collection<HttpMethod> getSupportedMethods(ResourceType resourcType) {
}

Set<HttpMethod> methods = new HashSet<HttpMethod>();
methods.add(HttpMethod.OPTIONS);

switch (resourcType) {
case COLLECTION:
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading

0 comments on commit e702853

Please sign in to comment.