DATAREST-381 - Improved HandlerMapping infrastructure to make sure controllers consider base URI.

Removed class-level @RequestMapping annotations as the controllers get picked up by standard Spring MVC and are exposed via the root even if a base URI is configured. Created custom @BaseUriAwareController and use that in AlpsController to make sure it doesn't get picked up by Spring MVC.
This commit is contained in:
Oliver Gierke
2014-09-04 14:29:59 +02:00
parent 40697a0e72
commit 1e0e3f0ad4
9 changed files with 228 additions and 122 deletions

View File

@@ -0,0 +1,37 @@
/*
* 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 java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
/**
* Annotation to declare a controller that declares request mappings to be augmented with a base URI in the Spring Data
* REST configuration.
*
* @author Oliver Gierke
*/
@Documented
@Component
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
public @interface BaseUriAwareController {
}

View File

@@ -0,0 +1,164 @@
/*
* 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.springframework.util.StringUtils.*;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.springframework.core.Ordered;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* Special {@link RequestMappingHandlerMapping} that uses the base URI configured in the
* {@link RepositoryRestConfiguration}, strips it from incoming requests in case they start with it and hands the
* altered URI to the superclass for normal handler method lookup.
*
* @author Oliver Gierke
*/
public class BaseUriAwareHandlerMapping extends RequestMappingHandlerMapping {
private final RepositoryRestConfiguration configuration;
/**
* Creates a new {@link BaseUriAwareHandlerMapping} using the given {@link RepositoryRestConfiguration}.
*
* @param configuration must not be {@literal null}.
*/
public BaseUriAwareHandlerMapping(RepositoryRestConfiguration configuration) {
Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!");
this.configuration = configuration;
setOrder(Ordered.LOWEST_PRECEDENCE - 150);
}
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod(java.lang.String, javax.servlet.http.HttpServletRequest)
*/
@Override
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
String acceptType = request.getHeader("Accept");
if (null == acceptType) {
acceptType = configuration.getDefaultMediaType().toString();
}
List<MediaType> acceptHeaderTypes = MediaType.parseMediaTypes(acceptType);
List<MediaType> acceptableTypes = new ArrayList<MediaType>();
for (MediaType mt : acceptHeaderTypes) {
if ("*".equals(mt.getType()) && "*".equals(mt.getSubtype()) || "application".equals(mt.getType())
&& "*".equals(mt.getSubtype())) {
mt = configuration.getDefaultMediaType();
}
if (!acceptableTypes.contains(mt)) {
acceptableTypes.add(mt);
}
}
if (acceptableTypes.size() > 1) {
acceptType = collectionToDelimitedString(acceptableTypes, ",");
} else if (acceptableTypes.size() == 1) {
acceptType = acceptableTypes.get(0).toString();
} else {
acceptType = configuration.getDefaultMediaType().toString();
}
String uri = new BaseUri(configuration.getBaseUri()).getRepositoryLookupPath(lookupPath);
if (uri == null) {
return null;
}
uri = StringUtils.hasText(uri) ? uri : "/";
HttpServletRequest wrapper = new DefaultAcceptTypeHttpServletRequest(request, acceptType, uri);
return supportsLookupPath(uri) ? super.lookupHandlerMethod(uri, wrapper) : null;
}
protected boolean supportsLookupPath(String lookupPath) {
return true;
}
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler(java.lang.Class)
*/
@Override
protected boolean isHandler(Class<?> beanType) {
return beanType.getAnnotation(BaseUriAwareController.class) != null;
}
private static class DefaultAcceptTypeHttpServletRequest extends HttpServletRequestWrapper {
private final String defaultAcceptType;
private final String requestUri;
private DefaultAcceptTypeHttpServletRequest(HttpServletRequest request, String defaultAcceptType) {
this(request, defaultAcceptType, null);
}
private DefaultAcceptTypeHttpServletRequest(HttpServletRequest request, String defaultAcceptType, String requestUri) {
super(request);
this.defaultAcceptType = defaultAcceptType;
this.requestUri = requestUri;
}
/*
* (non-Javadoc)
* @see javax.servlet.http.HttpServletRequestWrapper#getHeader(java.lang.String)
*/
@Override
public String getHeader(String name) {
if ("accept".equals(name.toLowerCase())) {
return defaultAcceptType;
} else {
return super.getHeader(name);
}
}
/*
* (non-Javadoc)
* @see javax.servlet.http.HttpServletRequestWrapper#getRequestURI()
*/
@Override
public String getRequestURI() {
return requestUri != null ? requestUri : super.getRequestURI();
}
/*
* (non-Javadoc)
* @see javax.servlet.http.HttpServletRequestWrapper#getServletPath()
*/
@Override
public String getServletPath() {
return requestUri != null ? requestUri : super.getServletPath();
}
}
}

View File

@@ -39,7 +39,6 @@ import org.springframework.web.bind.annotation.RequestMethod;
* @author Oliver Gierke
*/
@RepositoryRestController
@RequestMapping("/")
public class RepositoryController extends AbstractRepositoryRestController {
private final Repositories repositories;
@@ -76,7 +75,7 @@ public class RepositoryController extends AbstractRepositoryRestController {
* @return
* @since 2.2
*/
@RequestMapping(method = RequestMethod.OPTIONS)
@RequestMapping(value = "/", method = RequestMethod.OPTIONS)
public HttpEntity<?> optionsForRepositories() {
HttpHeaders headers = new HttpHeaders();
@@ -91,7 +90,7 @@ public class RepositoryController extends AbstractRepositoryRestController {
* @return
* @since 2.2
*/
@RequestMapping(method = RequestMethod.HEAD)
@RequestMapping(value = "/", method = RequestMethod.HEAD)
public ResponseEntity<?> headForRepositories() {
return new ResponseEntity<Object>(HttpStatus.NO_CONTENT);
}
@@ -101,7 +100,7 @@ public class RepositoryController extends AbstractRepositoryRestController {
*
* @return
*/
@RequestMapping(method = RequestMethod.GET)
@RequestMapping(value = "/", method = RequestMethod.GET)
public HttpEntity<RepositoryLinksResource> listRepositories() {
RepositoryLinksResource resource = new RepositoryLinksResource();

View File

@@ -168,7 +168,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
Iterable<?> results;
if (pageable != null) {
if (pageable.getPageable() != null) {
results = invoker.invokeFindAll(pageable.getPageable());
} else {
results = invoker.invokeFindAll(sort);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-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.
@@ -33,6 +33,6 @@ import org.springframework.stereotype.Component;
@Component
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@BaseUriAwareController
public @interface RepositoryRestController {
}

View File

@@ -63,6 +63,6 @@ public class RepositoryRestHandlerAdapter extends ResourceProcessorInvokingHandl
Class<?> controllerType = handlerMethod.getBeanType();
return AnnotationUtils.findAnnotation(controllerType, RepositoryRestController.class) != null;
return AnnotationUtils.findAnnotation(controllerType, BaseUriAwareController.class) != null;
}
}

View File

@@ -15,24 +15,15 @@
*/
package org.springframework.data.rest.webmvc;
import static org.springframework.util.StringUtils.*;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.webmvc.support.JpaHelper;
import org.springframework.http.MediaType;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
@@ -44,10 +35,9 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
* @author Jon Brisbin
* @author Oliver Gierke
*/
public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping {
public class RepositoryRestHandlerMapping extends BaseUriAwareHandlerMapping {
private final ResourceMappings mappings;
private final RepositoryRestConfiguration config;
private JpaHelper jpaHelper;
@@ -60,11 +50,12 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping {
*/
public RepositoryRestHandlerMapping(ResourceMappings mappings, RepositoryRestConfiguration config) {
super(config);
Assert.notNull(mappings, "ResourceMappings must not be null!");
Assert.notNull(config, "RepositoryRestConfiguration must not be null!");
this.mappings = mappings;
this.config = config;
setOrder(Ordered.LOWEST_PRECEDENCE - 100);
}
@@ -76,62 +67,19 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping {
this.jpaHelper = jpaHelper;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod(java.lang.String, javax.servlet.http.HttpServletRequest)
* @see org.springframework.data.rest.webmvc.BaseUriAwareHandlerMapping#supportsLookupPath(java.lang.String)
*/
@Override
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest origRequest) throws Exception {
protected boolean supportsLookupPath(String lookupPath) {
String acceptType = origRequest.getHeader("Accept");
if (null == acceptType) {
acceptType = config.getDefaultMediaType().toString();
if ("/".equals(lookupPath)) {
return true;
}
List<MediaType> acceptHeaderTypes = MediaType.parseMediaTypes(acceptType);
List<MediaType> acceptableTypes = new ArrayList<MediaType>();
for (MediaType mt : acceptHeaderTypes) {
if ("*".equals(mt.getType()) && "*".equals(mt.getSubtype()) || "application".equals(mt.getType())
&& "*".equals(mt.getSubtype())) {
mt = config.getDefaultMediaType();
}
if (!acceptableTypes.contains(mt)) {
acceptableTypes.add(mt);
}
}
if (acceptableTypes.size() > 1) {
acceptType = collectionToDelimitedString(acceptableTypes, ",");
} else if (acceptableTypes.size() == 1) {
acceptType = acceptableTypes.get(0).toString();
} else {
acceptType = config.getDefaultMediaType().toString();
}
String uri = new BaseUri(config.getBaseUri()).getRepositoryLookupPath(lookupPath);
if (uri == null) {
return null;
}
uri = StringUtils.hasText(uri) ? uri : "/";
HttpServletRequest request = new DefaultAcceptTypeHttpServletRequest(origRequest, acceptType, uri);
// Root request
if (uri.equals("/")) {
return super.lookupHandlerMethod("/", request);
}
String[] parts = uri.split("/");
if (mappings.exportsTopLevelResourceFor(parts[uri.startsWith("/") ? 1 : 0])) {
return super.lookupHandlerMethod(uri, request);
}
return null;
String[] parts = lookupPath.split("/");
return mappings.exportsTopLevelResourceFor(parts[lookupPath.startsWith("/") ? 1 : 0]);
}
/*
@@ -155,48 +103,4 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping {
}
}
}
private static class DefaultAcceptTypeHttpServletRequest extends HttpServletRequestWrapper {
private final String defaultAcceptType;
private final String requestUri;
private DefaultAcceptTypeHttpServletRequest(HttpServletRequest request, String defaultAcceptType) {
this(request, defaultAcceptType, null);
}
private DefaultAcceptTypeHttpServletRequest(HttpServletRequest request, String defaultAcceptType, String requestUri) {
super(request);
this.defaultAcceptType = defaultAcceptType;
this.requestUri = requestUri;
}
@Override
public String getHeader(String name) {
if ("accept".equals(name.toLowerCase())) {
return defaultAcceptType;
} else {
return super.getHeader(name);
}
}
/*
* (non-Javadoc)
* @see javax.servlet.http.HttpServletRequestWrapper#getRequestURI()
*/
@Override
public String getRequestURI() {
return requestUri != null ? requestUri : super.getRequestURI();
}
/*
* (non-Javadoc)
* @see javax.servlet.http.HttpServletRequestWrapper#getServletPath()
*/
@Override
public String getServletPath() {
return requestUri != null ? requestUri : super.getServletPath();
}
}
}

View File

@@ -26,8 +26,8 @@ import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.BaseUriAwareController;
import org.springframework.data.rest.webmvc.BaseUri;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.hateoas.alps.Alps;
@@ -48,11 +48,11 @@ import org.springframework.web.util.UriComponentsBuilder;
* @author Oliver Gierke
* @see http://alps.io
*/
@RepositoryRestController
@RequestMapping(AlpsController.ALPS_ROOT_MAPPING)
@BaseUriAwareController
public class AlpsController {
static final String ALPS_ROOT_MAPPING = "/alps";
static final String ALPS_RESOURCE_MAPPING = ALPS_ROOT_MAPPING + "/{repository}";
private final Repositories repositories;
private final ResourceMappings mappings;
@@ -83,7 +83,7 @@ public class AlpsController {
*
* @return
*/
@RequestMapping(value = { "", "/{repository}" }, method = OPTIONS)
@RequestMapping(value = { ALPS_ROOT_MAPPING, ALPS_RESOURCE_MAPPING }, method = OPTIONS)
HttpEntity<?> alpsOptions() {
verifyAlpsEnabled();
@@ -99,7 +99,7 @@ public class AlpsController {
*
* @return
*/
@RequestMapping(method = GET)
@RequestMapping(value = ALPS_ROOT_MAPPING, method = GET)
HttpEntity<Alps> alps() {
verifyAlpsEnabled();
@@ -132,7 +132,7 @@ public class AlpsController {
* @param information
* @return
*/
@RequestMapping(value = "/{repository}", method = GET)
@RequestMapping(value = ALPS_RESOURCE_MAPPING, method = GET)
HttpEntity<RootResourceInformation> descriptor(RootResourceInformation information) {
verifyAlpsEnabled();

View File

@@ -66,7 +66,9 @@ import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.data.rest.core.support.DomainObjectMerger;
import org.springframework.data.rest.core.support.RepositoryRelProvider;
import org.springframework.data.rest.core.util.UUIDConverter;
import org.springframework.data.rest.webmvc.BaseUriAwareController;
import org.springframework.data.rest.webmvc.BaseUri;
import org.springframework.data.rest.webmvc.BaseUriAwareHandlerMapping;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping;
@@ -135,7 +137,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL)
@ComponentScan(basePackageClasses = RepositoryRestController.class,
includeFilters = @Filter(RepositoryRestController.class), useDefaultFilters = false)
includeFilters = @Filter(BaseUriAwareController.class), useDefaultFilters = false)
@ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml")
@Import(SpringDataJacksonConfiguration.class)
public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebConfiguration {
@@ -500,7 +502,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Bean
public RequestMappingHandlerMapping fallbackMapping() {
return new RequestMappingHandlerMapping();
return new BaseUriAwareHandlerMapping(config());
}
@Bean