From 55cf4cd9e098a7d36ff03d82f5afcb9032011483 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 15 Oct 2014 11:58:26 +0200 Subject: [PATCH] DATAREST-429 - Improved handling of custom base paths. Introduced a new property basePath on RepositoryRestConfiguration to allow a smooth migration to a new non-absolute base path configuration model to customize the URIs under which repository resources are exposed. We're deprecating the current baseUri property as it previously supported absolute URIs. These are still supported but a warning is logged to ping users to move to the new base path based configuration. We now use the base path to dynamically augment the mappings detected on our controllers with the path configured. --- .../config/RepositoryRestConfiguration.java | 51 +- ...ller.java => BasePathAwareController.java} | 4 +- .../webmvc/BasePathAwareHandlerMapping.java | 500 ++++++++++++++++++ .../webmvc/BaseUriAwareHandlerMapping.java | 164 ------ .../rest/webmvc/RepositoryController.java | 8 +- .../rest/webmvc/RepositoryRestController.java | 4 +- .../webmvc/RepositoryRestHandlerAdapter.java | 17 +- .../webmvc/RepositoryRestHandlerMapping.java | 35 +- .../data/rest/webmvc/alps/AlpsController.java | 6 +- .../RepositoryRestMvcConfiguration.java | 11 +- .../AugmentingHandlerMappingUnitTest.java | 73 +++ ...RepositoryRestHandlerMappingUnitTests.java | 61 ++- 12 files changed, 713 insertions(+), 221 deletions(-) rename spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/{BaseUriAwareController.java => BasePathAwareController.java} (92%) create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java delete mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriAwareHandlerMapping.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AugmentingHandlerMappingUnitTest.java diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java index 85ce70ea4..d8a9f3c76 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -20,9 +20,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * @author Jon Brisbin @@ -31,7 +34,11 @@ import org.springframework.util.Assert; @SuppressWarnings("deprecation") public class RepositoryRestConfiguration { - private URI baseUri = URI.create(""); + private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryRestConfiguration.class); + private static final URI NO_URI = URI.create(""); + + private URI baseUri = NO_URI; + private URI basePath = NO_URI; private int defaultPageSize = 20; private int maxPageSize = 1000; private String pageParamName = "page"; @@ -76,16 +83,36 @@ public class RepositoryRestConfiguration { * @return The base URI. */ public URI getBaseUri() { - return baseUri; + return basePath != NO_URI ? basePath : baseUri; + } + + /** + * The base path to expose repository resources under. + * + * @return the basePath + */ + public URI getBasePath() { + return basePath; } /** * The base URI against which the exporter should calculate its links. * * @param baseUri must not be {@literal null}. + * @deprecated use {@link #setBasePath(String)} instead. */ + @Deprecated public RepositoryRestConfiguration setBaseUri(URI baseUri) { + Assert.notNull(baseUri, "The base URI cannot be null."); + + LOGGER.warn("Configuring a base URI has been deprecated. Use basePath property instead!"); + + if (baseUri.isAbsolute()) { + LOGGER + .warn("Using absolute base URIs will not be supported as of Spring Data REST 2.3! Be sure to switch to configuring a base path!"); + } + this.baseUri = baseUri; return this; } @@ -94,11 +121,25 @@ public class RepositoryRestConfiguration { * The base URI against which the exporter should calculate its links. * * @param baseUri must not be {@literal null}. + * @deprecated use {@link #setBasePath(String)} instead. */ + @Deprecated public RepositoryRestConfiguration setBaseUri(String baseUri) { Assert.notNull(baseUri, "The base URI cannot be null."); - this.baseUri = URI.create(baseUri); - return this; + return setBaseUri(URI.create(baseUri)); + } + + /** + * Configures the base path to be used by Spring Data REST to expose repository resources. + * + * @param basePath the basePath to set + */ + public void setBasePath(String basePath) { + + basePath = StringUtils.trimTrailingCharacter(basePath, '/'); + this.basePath = URI.create(basePath.startsWith("/") ? basePath : "/".concat(basePath)); + + Assert.isTrue(!this.basePath.isAbsolute(), "Absolute URIs are not supported as base path!"); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriAwareController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareController.java similarity index 92% rename from spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriAwareController.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareController.java index cedb6d2ee..1c02c908c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriAwareController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareController.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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,5 +33,5 @@ import org.springframework.stereotype.Component; @Component @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) -public @interface BaseUriAwareController { +public @interface BasePathAwareController { } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java new file mode 100644 index 000000000..1e2c41f4d --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java @@ -0,0 +1,500 @@ +/* + * Copyright 2014-2015 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.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.net.URI; +import java.security.Principal; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.Part; + +import org.springframework.core.Ordered; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.util.Assert; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.UrlPathHelper; + +/** + * A {@link RequestMappingHandlerMapping} that augments the request mappings + * + * @author Oliver Gierke + */ +public class BasePathAwareHandlerMapping extends RequestMappingHandlerMapping { + + private static final UrlPathHelper URL_PATH_HELPER = new UrlPathHelper(); + + private final RepositoryRestConfiguration configuration; + + private String prefix; + + /** + * Creates a new {@link BasePathAwareHandlerMapping} using the given {@link RepositoryRestConfiguration}. + * + * @param configuration must not be {@literal null}. + */ + public BasePathAwareHandlerMapping(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.mvc.method.annotation.RequestMappingHandlerMapping#getMappingForMethod(java.lang.reflect.Method, java.lang.Class) + */ + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + + RequestMappingInfo info = super.getMappingForMethod(method, handlerType); + + if (info == null) { + return null; + } + + PatternsRequestCondition patternsCondition = info.getPatternsCondition(); + + Set patterns = patternsCondition.getPatterns(); + String[] augmentedPatterns = new String[patterns.size()]; + int count = 0; + + for (String pattern : patterns) { + augmentedPatterns[count++] = prefix.concat(pattern); + } + + PatternsRequestCondition condition = new PatternsRequestCondition(augmentedPatterns, getUrlPathHelper(), + getPathMatcher(), useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()); + + return new RequestMappingInfo(condition, info.getMethodsCondition(), info.getParamsCondition(), + info.getHeadersCondition(), info.getConsumesCondition(), info.getProducesCondition(), info.getCustomCondition()); + } + + /* + * (non-Javadoc) + * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler(java.lang.Class) + */ + @Override + protected boolean isHandler(Class beanType) { + return beanType.getAnnotation(BasePathAwareController.class) != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + + URI baseUri = configuration.getBaseUri(); + + if (baseUri.isAbsolute()) { + HttpServletRequest request = new UriAwareHttpServletRequest(getServletContext(), baseUri); + this.prefix = URL_PATH_HELPER.getPathWithinApplication(request); + } else { + this.prefix = baseUri.toString(); + } + + super.afterPropertiesSet(); + } + + private class UriAwareHttpServletRequest implements HttpServletRequest { + + private final ServletContext context; + private final String path; + + /** + * @param context + * @param uri + */ + public UriAwareHttpServletRequest(ServletContext context, URI uri) { + this.context = context; + this.path = uri.getPath(); + } + + /* + * (non-Javadoc) + * @see javax.servlet.ServletRequest#getAttribute(java.lang.String) + */ + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see javax.servlet.ServletRequest#getCharacterEncoding() + */ + @Override + public String getCharacterEncoding() { + return null; + } + + @Override + public void setCharacterEncoding(String env) throws UnsupportedEncodingException { + throw new UnsupportedOperationException(); + } + + @Override + public int getContentLength() { + throw new UnsupportedOperationException(); + } + + @Override + public String getContentType() { + throw new UnsupportedOperationException(); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public String getParameter(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Enumeration getParameterNames() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getParameterValues(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Map getParameterMap() { + throw new UnsupportedOperationException(); + } + + @Override + public String getProtocol() { + throw new UnsupportedOperationException(); + } + + @Override + public String getScheme() { + throw new UnsupportedOperationException(); + } + + @Override + public String getServerName() { + throw new UnsupportedOperationException(); + } + + @Override + public int getServerPort() { + throw new UnsupportedOperationException(); + } + + @Override + public BufferedReader getReader() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public String getRemoteAddr() { + throw new UnsupportedOperationException(); + } + + @Override + public String getRemoteHost() { + throw new UnsupportedOperationException(); + } + + @Override + public void setAttribute(String name, Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAttribute(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Locale getLocale() { + throw new UnsupportedOperationException(); + } + + @Override + public Enumeration getLocales() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSecure() { + throw new UnsupportedOperationException(); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + throw new UnsupportedOperationException(); + } + + @Override + public String getRealPath(String path) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRemotePort() { + throw new UnsupportedOperationException(); + } + + @Override + public String getLocalName() { + throw new UnsupportedOperationException(); + } + + @Override + public String getLocalAddr() { + throw new UnsupportedOperationException(); + } + + @Override + public int getLocalPort() { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see javax.servlet.ServletRequest#getServletContext() + */ + @Override + public ServletContext getServletContext() { + return context; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + throw new UnsupportedOperationException(); + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) + throws IllegalStateException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isAsyncStarted() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isAsyncSupported() { + throw new UnsupportedOperationException(); + } + + @Override + public AsyncContext getAsyncContext() { + throw new UnsupportedOperationException(); + } + + @Override + public DispatcherType getDispatcherType() { + throw new UnsupportedOperationException(); + } + + @Override + public String getAuthType() { + throw new UnsupportedOperationException(); + } + + @Override + public Cookie[] getCookies() { + throw new UnsupportedOperationException(); + } + + @Override + public long getDateHeader(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public String getHeader(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Enumeration getHeaders(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Enumeration getHeaderNames() { + throw new UnsupportedOperationException(); + } + + @Override + public int getIntHeader(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public String getMethod() { + throw new UnsupportedOperationException(); + } + + @Override + public String getPathInfo() { + throw new UnsupportedOperationException(); + } + + @Override + public String getPathTranslated() { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see javax.servlet.http.HttpServletRequest#getContextPath() + */ + @Override + public String getContextPath() { + return context.getContextPath(); + } + + @Override + public String getQueryString() { + throw new UnsupportedOperationException(); + } + + @Override + public String getRemoteUser() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUserInRole(String role) { + throw new UnsupportedOperationException(); + } + + @Override + public Principal getUserPrincipal() { + throw new UnsupportedOperationException(); + } + + @Override + public String getRequestedSessionId() { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see javax.servlet.http.HttpServletRequest#getRequestURI() + */ + @Override + public String getRequestURI() { + return path; + } + + @Override + public StringBuffer getRequestURL() { + throw new UnsupportedOperationException(); + } + + @Override + public String getServletPath() { + throw new UnsupportedOperationException(); + } + + @Override + public HttpSession getSession(boolean create) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpSession getSession() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isRequestedSessionIdValid() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isRequestedSessionIdFromURL() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isRequestedSessionIdFromUrl() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void login(String username, String password) throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void logout() throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getParts() throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriAwareHandlerMapping.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriAwareHandlerMapping.java deleted file mode 100644 index 6c2303e47..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriAwareHandlerMapping.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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 acceptHeaderTypes = MediaType.parseMediaTypes(acceptType); - List acceptableTypes = new ArrayList(); - - 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(); - } - } -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java index 22850be8e..811328d02 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -75,7 +75,7 @@ public class RepositoryController extends AbstractRepositoryRestController { * @return * @since 2.2 */ - @RequestMapping(value = "/", method = RequestMethod.OPTIONS) + @RequestMapping(value = { "/", "" }, method = RequestMethod.OPTIONS) public HttpEntity optionsForRepositories() { HttpHeaders headers = new HttpHeaders(); @@ -90,7 +90,7 @@ public class RepositoryController extends AbstractRepositoryRestController { * @return * @since 2.2 */ - @RequestMapping(value = "/", method = RequestMethod.HEAD) + @RequestMapping(value = { "/", "" }, method = RequestMethod.HEAD) public ResponseEntity headForRepositories() { return new ResponseEntity(HttpStatus.NO_CONTENT); } @@ -100,7 +100,7 @@ public class RepositoryController extends AbstractRepositoryRestController { * * @return */ - @RequestMapping(value = "/", method = RequestMethod.GET) + @RequestMapping(value = { "/", "" }, method = RequestMethod.GET) public HttpEntity listRepositories() { RepositoryLinksResource resource = new RepositoryLinksResource(); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java index fc7140d98..a080e244c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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 +@BasePathAwareController public @interface RepositoryRestController { } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java index 69c096a04..b0bd89351 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-2015 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.util.List; @@ -63,6 +78,6 @@ public class RepositoryRestHandlerAdapter extends ResourceProcessorInvokingHandl Class controllerType = handlerMethod.getBeanType(); - return AnnotationUtils.findAnnotation(controllerType, BaseUriAwareController.class) != null; + return AnnotationUtils.findAnnotation(controllerType, BasePathAwareController.class) != null; } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java index 26af6e8ce..d671fefd5 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-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. @@ -16,6 +16,10 @@ package org.springframework.data.rest.webmvc; import java.util.List; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; @@ -24,6 +28,8 @@ import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.support.JpaHelper; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.util.Assert; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; /** @@ -35,9 +41,10 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl * @author Jon Brisbin * @author Oliver Gierke */ -public class RepositoryRestHandlerMapping extends BaseUriAwareHandlerMapping { +public class RepositoryRestHandlerMapping extends BasePathAwareHandlerMapping { private final ResourceMappings mappings; + private final RepositoryRestConfiguration configuration; private JpaHelper jpaHelper; @@ -56,6 +63,7 @@ public class RepositoryRestHandlerMapping extends BaseUriAwareHandlerMapping { Assert.notNull(config, "RepositoryRestConfiguration must not be null!"); this.mappings = mappings; + this.configuration = config; setOrder(Ordered.LOWEST_PRECEDENCE - 100); } @@ -69,17 +77,28 @@ public class RepositoryRestHandlerMapping extends BaseUriAwareHandlerMapping { /* * (non-Javadoc) - * @see org.springframework.data.rest.webmvc.BaseUriAwareHandlerMapping#supportsLookupPath(java.lang.String) + * @see org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod(java.lang.String, javax.servlet.http.HttpServletRequest) */ @Override - protected boolean supportsLookupPath(String lookupPath) { + protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { - if ("/".equals(lookupPath)) { - return true; + HandlerMethod handlerMethod = super.lookupHandlerMethod(lookupPath, request); + + if (handlerMethod == null) { + return null; } - String[] parts = lookupPath.split("/"); - return mappings.exportsTopLevelResourceFor(parts[lookupPath.startsWith("/") ? 1 : 0]); + return new BaseUri(configuration.getBaseUri()).getRepositoryLookupPath(lookupPath) == null ? null : handlerMethod; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping#handleNoMatch(java.util.Set, java.lang.String, javax.servlet.http.HttpServletRequest) + */ + @Override + protected HandlerMethod handleNoMatch(Set requestMappingInfos, String lookupPath, + HttpServletRequest request) throws ServletException { + return null; } /* diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java index 77a5c62c6..685c7f520 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 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. @@ -26,7 +26,7 @@ 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.BasePathAwareController; import org.springframework.data.rest.webmvc.BaseUri; import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.data.rest.webmvc.RootResourceInformation; @@ -48,7 +48,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Oliver Gierke * @see http://alps.io */ -@BaseUriAwareController +@BasePathAwareController public class AlpsController { static final String ALPS_ROOT_MAPPING = "/alps"; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index b086ebb9b..faf9b6952 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 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. @@ -65,9 +65,9 @@ import org.springframework.data.rest.core.mapping.ResourceMappings; 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.webmvc.BasePathAwareController; +import org.springframework.data.rest.webmvc.BasePathAwareHandlerMapping; import org.springframework.data.rest.webmvc.BaseUri; -import org.springframework.data.rest.webmvc.BaseUriAwareController; -import org.springframework.data.rest.webmvc.BaseUriAwareHandlerMapping; import org.springframework.data.rest.webmvc.RepositoryRestController; import org.springframework.data.rest.webmvc.RepositoryRestExceptionHandler; import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter; @@ -139,7 +139,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; @Configuration @EnableHypermediaSupport(type = HypermediaType.HAL) @ComponentScan(basePackageClasses = RepositoryRestController.class, - includeFilters = @Filter(BaseUriAwareController.class), useDefaultFilters = false) + includeFilters = @Filter(BasePathAwareController.class), useDefaultFilters = false) @ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml") @Import(SpringDataJacksonConfiguration.class) public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebConfiguration { @@ -239,6 +239,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon RepositoryRestConfiguration config = new RepositoryRestConfiguration(configuration, metadataConfiguration()); configureRepositoryRestConfiguration(config); + return config; } @@ -519,7 +520,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Bean public RequestMappingHandlerMapping fallbackMapping() { - return new BaseUriAwareHandlerMapping(config()); + return new BasePathAwareHandlerMapping(config()); } @Bean diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AugmentingHandlerMappingUnitTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AugmentingHandlerMappingUnitTest.java new file mode 100644 index 000000000..4dfb4c5c7 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AugmentingHandlerMappingUnitTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2014-2015 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.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; + +/** + * Unit tests for {@link BasePathAwareHandlerMapping}. + * + * @author Oliver Gierke + */ +public class AugmentingHandlerMappingUnitTest { + + @Configuration + static class Config { + + @Bean + SampleController sampleController() { + return new SampleController(); + } + } + + @Test + @SuppressWarnings("deprecation") + public void augmentsRequestMappingsWithBaseUriFromConfiguration() { + + RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); + configuration.setBaseUri("api"); + + BasePathAwareHandlerMapping mapping = new BasePathAwareHandlerMapping(configuration); + mapping.setApplicationContext(new AnnotationConfigApplicationContext(Config.class)); + mapping.afterPropertiesSet(); + + Map handlerMethods = mapping.getHandlerMethods(); + + for (RequestMappingInfo info : handlerMethods.keySet()) { + assertThat(info.getPatternsCondition().getPatterns(), hasItem(startsWith("/api"))); + } + } + + @Controller + static class SampleController { + + @RequestMapping("/foo") + void someMethod() {} + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java index d2760c919..c96c0e38c 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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. @@ -20,21 +20,20 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; -import java.net.URI; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.data.domain.Sort; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; import org.springframework.data.rest.webmvc.support.DefaultedPageable; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.method.HandlerMethod; /** @@ -45,7 +44,13 @@ import org.springframework.web.method.HandlerMethod; @RunWith(MockitoJUnitRunner.class) public class RepositoryRestHandlerMappingUnitTests { - static final ApplicationContext CONTEXT = new AnnotationConfigApplicationContext(RepositoryRestMvcConfiguration.class); + static final AnnotationConfigWebApplicationContext CONTEXT = new AnnotationConfigWebApplicationContext(); + + static { + CONTEXT.register(RepositoryRestMvcConfiguration.class); + CONTEXT.setServletContext(new MockServletContext()); + CONTEXT.refresh(); + } @Mock ResourceMappings mappings; @@ -61,7 +66,6 @@ public class RepositoryRestHandlerMappingUnitTests { handlerMapping = new RepositoryRestHandlerMapping(mappings, configuration); handlerMapping.setApplicationContext(CONTEXT); - handlerMapping.afterPropertiesSet(); mockRequest = new MockHttpServletRequest(); @@ -85,6 +89,8 @@ public class RepositoryRestHandlerMappingUnitTests { */ @Test public void returnsNullForUriNotMapped() throws Exception { + + handlerMapping.afterPropertiesSet(); assertThat(handlerMapping.lookupHandlerMethod("/foo", mockRequest), is(nullValue())); } @@ -97,6 +103,7 @@ public class RepositoryRestHandlerMappingUnitTests { when(mappings.exportsTopLevelResourceFor("people")).thenReturn(true); mockRequest = new MockHttpServletRequest("GET", "/people"); + handlerMapping.afterPropertiesSet(); HandlerMethod method = handlerMapping.lookupHandlerMethod("/people", mockRequest); assertThat(method, is(notNullValue())); @@ -112,7 +119,8 @@ public class RepositoryRestHandlerMappingUnitTests { when(mappings.exportsTopLevelResourceFor("people")).thenReturn(true); mockRequest = new MockHttpServletRequest("GET", "/base/people"); - configuration.setBaseUri(URI.create("base")); + configuration.setBasePath("/base"); + handlerMapping.afterPropertiesSet(); HandlerMethod method = handlerMapping.lookupHandlerMethod("/base/people", mockRequest); @@ -129,7 +137,8 @@ public class RepositoryRestHandlerMappingUnitTests { when(mappings.exportsTopLevelResourceFor("people")).thenReturn(true); mockRequest = new MockHttpServletRequest("GET", "/base"); - configuration.setBaseUri(URI.create("base")); + configuration.setBasePath("/base"); + handlerMapping.afterPropertiesSet(); HandlerMethod method = handlerMapping.lookupHandlerMethod("/base", mockRequest); @@ -141,14 +150,14 @@ public class RepositoryRestHandlerMappingUnitTests { * @see DATAREST-276 */ @Test + @SuppressWarnings("deprecation") public void returnsRepositoryHandlerMethodForAbsoluteBaseUri() throws Exception { - String baseUri = "http://localhost/base"; - when(mappings.exportsTopLevelResourceFor("people")).thenReturn(true); - mockRequest = new MockHttpServletRequest("GET", baseUri.concat("/people/")); + mockRequest = new MockHttpServletRequest("GET", "/base/people/"); - configuration.setBaseUri(URI.create(baseUri)); + configuration.setBaseUri("http://localhost/base"); + handlerMapping.afterPropertiesSet(); HandlerMethod method = handlerMapping.lookupHandlerMethod("/base/people/", mockRequest); @@ -160,18 +169,17 @@ public class RepositoryRestHandlerMappingUnitTests { * @see DATAREST-276 */ @Test + @SuppressWarnings("deprecation") public void returnsRepositoryHandlerMethodForAbsoluteBaseUriWithServletMapping() throws Exception { - String baseUri = "http://localhost/base"; - String uri = baseUri.concat("/people/"); - when(mappings.exportsTopLevelResourceFor("people")).thenReturn(true); - mockRequest = new MockHttpServletRequest("GET", uri); - mockRequest.setServletPath(uri); + mockRequest = new MockHttpServletRequest("GET", "/base/people"); + mockRequest.setServletPath("/base/people"); - configuration.setBaseUri(URI.create(baseUri)); + configuration.setBaseUri("http://localhost/base"); + handlerMapping.afterPropertiesSet(); - HandlerMethod method = handlerMapping.lookupHandlerMethod("/base/people/", mockRequest); + HandlerMethod method = handlerMapping.lookupHandlerMethod("/base/people", mockRequest); assertThat(method, is(notNullValue())); assertThat(method.getMethod(), is(listEntitiesMethod)); @@ -181,18 +189,16 @@ public class RepositoryRestHandlerMappingUnitTests { * @see DATAREST-276 */ @Test + @SuppressWarnings("deprecation") public void refrainsFromMappingIfTheRequestDoesNotPointIntoAbsolutelyDefinedUriSpace() throws Exception { - String baseUri = "http://localhost/base"; - String uri = baseUri.concat("/people/"); - when(mappings.exportsTopLevelResourceFor("people")).thenReturn(true); - mockRequest = new MockHttpServletRequest("GET", uri); - mockRequest.setServletPath(uri); + mockRequest = new MockHttpServletRequest("GET", "/servlet-path"); + mockRequest.setServletPath("/servlet-path"); - configuration.setBaseUri(URI.create(baseUri)); + configuration.setBaseUri("http://localhost/base"); - HandlerMethod method = handlerMapping.lookupHandlerMethod("/people", mockRequest); + HandlerMethod method = handlerMapping.lookupHandlerMethod("/servlet-path", mockRequest); assertThat(method, is(nullValue())); } @@ -201,6 +207,7 @@ public class RepositoryRestHandlerMappingUnitTests { * @see DATAREST-276 */ @Test + @SuppressWarnings("deprecation") public void refrainsFromMappingWhenUrisDontMatch() throws Exception { String baseUri = "foo"; @@ -210,7 +217,7 @@ public class RepositoryRestHandlerMappingUnitTests { mockRequest = new MockHttpServletRequest("GET", uri); mockRequest.setServletPath(uri); - configuration.setBaseUri(URI.create(baseUri)); + configuration.setBaseUri(baseUri); HandlerMethod method = handlerMapping.lookupHandlerMethod("/people", mockRequest);