diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java index f1bc1416923..76eae2ba52c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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,7 +16,11 @@ package org.springframework.web.reactive.config; +import java.util.LinkedHashMap; +import java.util.Map; + import org.springframework.lang.Nullable; +import org.springframework.web.method.HandlerTypePredicate; /** * Assist with configuring {@code HandlerMapping}'s with path matching options. @@ -34,6 +38,9 @@ public class PathMatchConfigurer { @Nullable private Boolean caseSensitiveMatch; + @Nullable + private Map pathPrefixes; + /** * Whether to match to URLs irrespective of their case. @@ -55,6 +62,22 @@ public class PathMatchConfigurer { return this; } + /** + * Configure a path prefix to apply to matching controller methods. + *

Prefixes are used to enrich the mappings of every {@code @RequestMapping} + * method whose controller type is matched by the corresponding + * {@link HandlerTypePredicate}. The prefix for the first matching predicate + * is used. + * @param prefix the path prefix to apply + * @param predicate a predicate for matching controller types + * @since 5.1 + */ + public PathMatchConfigurer addPathPrefix(String prefix, HandlerTypePredicate predicate) { + this.pathPrefixes = this.pathPrefixes == null ? new LinkedHashMap<>() : this.pathPrefixes; + this.pathPrefixes.put(prefix, predicate); + return this; + } + @Nullable protected Boolean isUseTrailingSlashMatch() { return this.trailingSlashMatch; @@ -65,4 +88,8 @@ public class PathMatchConfigurer { return this.caseSensitiveMatch; } + @Nullable + protected Map getPathPrefixes() { + return this.pathPrefixes; + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index c636d929a28..0ef94653065 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -44,6 +44,7 @@ import org.springframework.validation.Validator; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; @@ -119,14 +120,22 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { mapping.setCorsConfigurations(getCorsConfigurations()); PathMatchConfigurer configurer = getPathMatchConfigurer(); + Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch(); - Boolean useCaseSensitiveMatch = configurer.isUseCaseSensitiveMatch(); if (useTrailingSlashMatch != null) { mapping.setUseTrailingSlashMatch(useTrailingSlashMatch); } + + Boolean useCaseSensitiveMatch = configurer.isUseCaseSensitiveMatch(); if (useCaseSensitiveMatch != null) { mapping.setUseCaseSensitiveMatch(useCaseSensitiveMatch); } + + Map pathPrefixes = configurer.getPathPrefixes(); + if (pathPrefixes != null) { + mapping.setPathPrefixes(pathPrefixes); + } + return mapping; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index 8f02444d16c..5cf137f1f1e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -18,6 +18,9 @@ package org.springframework.web.reactive.result.method.annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -25,11 +28,13 @@ import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; @@ -48,6 +53,8 @@ import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerM public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements EmbeddedValueResolverAware { + private final Map pathPrefixes = new LinkedHashMap<>(); + private RequestedContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder().build(); @Nullable @@ -56,6 +63,22 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); + /** + * Configure path prefixes to apply to controller methods. + *

Prefixes are used to enrich the mappings of every {@code @RequestMapping} + * method whose controller type is matched by the corresponding + * {@link HandlerTypePredicate} in the map. The prefix for the first matching + * predicate is used, assuming the input map has predictable order. + * @param prefixes a map with path prefixes as key + * @since 5.1 + */ + public void setPathPrefixes(Map prefixes) { + this.pathPrefixes.clear(); + prefixes.entrySet().stream() + .filter(entry -> StringUtils.hasText(entry.getKey())) + .forEach(entry -> this.pathPrefixes.put(entry.getKey(), entry.getValue())); + } + /** * Set the {@link RequestedContentTypeResolver} to use to determine requested media types. * If not set, the default constructor is used. @@ -80,6 +103,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } + /** + * The configured path prefixes as a read-only, possibly empty map. + * @since 5.1 + */ + public Map getPathPrefixes() { + return Collections.unmodifiableMap(this.pathPrefixes); + } + /** * Return the configured {@link RequestedContentTypeResolver}. */ @@ -113,6 +144,16 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi if (typeInfo != null) { info = typeInfo.combine(info); } + for (Map.Entry entry : this.pathPrefixes.entrySet()) { + if (entry.getValue().test(handlerType)) { + String prefix = entry.getKey(); + if (this.embeddedValueResolver != null) { + prefix = this.embeddedValueResolver.resolveStringValue(prefix); + } + info = RequestMappingInfo.paths(prefix).build().combine(info); + break; + } + } } return info; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java index 182b7e8bb46..ea9a9221b67 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java @@ -18,8 +18,10 @@ package org.springframework.web.reactive.config; import java.lang.reflect.Field; import java.nio.ByteBuffer; +import java.security.Principal; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.xml.bind.annotation.XmlRootElement; import org.junit.Test; @@ -47,11 +49,17 @@ import org.springframework.util.MimeTypeUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; import org.springframework.validation.Validator; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.bind.support.WebExchangeDataBinder; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.handler.AbstractUrlHandlerMapping; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; @@ -67,6 +75,7 @@ import org.springframework.web.server.WebHandler; import org.springframework.web.util.pattern.PathPatternParser; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClassWithGenerics; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; @@ -110,7 +119,7 @@ public class WebFluxConfigurationSupportTests { } @Test - public void customPathMatchConfig() throws Exception { + public void customPathMatchConfig() { ApplicationContext context = loadConfig(CustomPatchMatchConfig.class); final Field field = ReflectionUtils.findField(PathPatternParser.class, "matchOptionalTrailingSeparator"); ReflectionUtils.makeAccessible(field); @@ -123,6 +132,11 @@ public class WebFluxConfigurationSupportTests { assertNotNull(patternParser); boolean matchOptionalTrailingSlash = (boolean) ReflectionUtils.getField(field, patternParser); assertFalse(matchOptionalTrailingSlash); + + Map map = mapping.getHandlerMethods(); + assertEquals(1, map.size()); + assertEquals(Collections.singleton(new PathPatternParser().parse("/api/user/{id}")), + map.keySet().iterator().next().getPatternsCondition().getPatterns()); } @Test @@ -295,6 +309,23 @@ public class WebFluxConfigurationSupportTests { @Override public void configurePathMatching(PathMatchConfigurer configurer) { configurer.setUseTrailingSlashMatch(false); + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); + } + + @Bean + UserController userController() { + return new UserController(); + } + } + + + @RestController + @RequestMapping("/user") + static class UserController { + + @GetMapping("/{id}") + public Principal getUser() { + return mock(Principal.class); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index eb1c6ca7d8a..1835500edf2 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -21,7 +21,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.security.Principal; import java.util.ArrayList; +import java.util.Collections; import java.util.Set; import org.junit.Before; @@ -37,13 +39,17 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; /** * Unit tests for {@link RequestMappingHandlerMapping}. @@ -64,9 +70,7 @@ public class RequestMappingHandlerMappingTests { @Test public void resolveEmbeddedValuesInPatterns() { - this.handlerMapping.setEmbeddedValueResolver( - value -> "/${pattern}/bar".equals(value) ? "/foo/bar" : value - ); + this.handlerMapping.setEmbeddedValueResolver(value -> "/${pattern}/bar".equals(value) ? "/foo/bar" : value); String[] patterns = new String[] { "/foo", "/${pattern}/bar" }; String[] result = this.handlerMapping.resolveEmbeddedValuesInPatterns(patterns); @@ -74,6 +78,20 @@ public class RequestMappingHandlerMappingTests { assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result); } + @Test + public void pathPrefix() throws NoSuchMethodException { + this.handlerMapping.setEmbeddedValueResolver(value -> "/${prefix}".equals(value) ? "/api" : value); + this.handlerMapping.setPathPrefixes(Collections.singletonMap( + "/${prefix}", HandlerTypePredicate.forAnnotation(RestController.class))); + + Method method = UserController.class.getMethod("getUser"); + RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, UserController.class); + + assertNotNull(info); + assertEquals(Collections.singleton(new PathPatternParser().parse("/api/user/{id}")), + info.getPatternsCondition().getPatterns()); + } + @Test public void resolveRequestMappingViaComposedAnnotation() throws Exception { RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST); @@ -191,4 +209,15 @@ public class RequestMappingHandlerMappingTests { String[] value() default {}; } + + @RestController + @RequestMapping("/user") + static class UserController { + + @GetMapping("/{id}") + public Principal getUser() { + return mock(Principal.class); + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java index e8db05bc595..6525b60c4f8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java @@ -16,8 +16,12 @@ package org.springframework.web.servlet.config.annotation; +import java.util.LinkedHashMap; +import java.util.Map; + import org.springframework.lang.Nullable; import org.springframework.util.PathMatcher; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.util.UrlPathHelper; /** @@ -53,6 +57,9 @@ public class PathMatchConfigurer { @Nullable private PathMatcher pathMatcher; + @Nullable + private Map pathPrefixes; + /** * Whether to use suffix pattern match (".*") when matching patterns to @@ -110,6 +117,22 @@ public class PathMatchConfigurer { return this; } + /** + * Configure a path prefix to apply to matching controller methods. + *

Prefixes are used to enrich the mappings of every {@code @RequestMapping} + * method whose controller type is matched by the corresponding + * {@link HandlerTypePredicate}. The prefix for the first matching predicate + * is used. + * @param prefix the prefix to apply + * @param predicate a predicate for matching controller types + * @since 5.1 + */ + public PathMatchConfigurer addPathPrefix(String prefix, HandlerTypePredicate predicate) { + this.pathPrefixes = this.pathPrefixes == null ? new LinkedHashMap<>() : this.pathPrefixes; + this.pathPrefixes.put(prefix, predicate); + return this; + } + @Nullable public Boolean isUseSuffixPatternMatch() { @@ -136,4 +159,8 @@ public class PathMatchConfigurer { return this.pathMatcher; } + @Nullable + protected Map getPathPrefixes() { + return this.pathPrefixes; + } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 8ce220f0260..d85b5bcc7c4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -70,6 +70,7 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.ServletContextAware; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.method.support.CompositeUriComponentsContributor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -284,15 +285,18 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv mapping.setCorsConfigurations(getCorsConfigurations()); PathMatchConfigurer configurer = getPathMatchConfigurer(); + Boolean useSuffixPatternMatch = configurer.isUseSuffixPatternMatch(); - Boolean useRegisteredSuffixPatternMatch = configurer.isUseRegisteredSuffixPatternMatch(); - Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch(); if (useSuffixPatternMatch != null) { mapping.setUseSuffixPatternMatch(useSuffixPatternMatch); } + + Boolean useRegisteredSuffixPatternMatch = configurer.isUseRegisteredSuffixPatternMatch(); if (useRegisteredSuffixPatternMatch != null) { mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch); } + + Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch(); if (useTrailingSlashMatch != null) { mapping.setUseTrailingSlashMatch(useTrailingSlashMatch); } @@ -307,6 +311,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv mapping.setPathMatcher(pathMatcher); } + Map pathPrefixes = configurer.getPathPrefixes(); + if (pathPrefixes != null) { + mapping.setPathPrefixes(pathPrefixes); + } + return mapping; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 565ddeca4e9..5297bc3912e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -18,7 +18,10 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; @@ -28,12 +31,14 @@ import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.MatchableHandlerMapping; import org.springframework.web.servlet.handler.RequestMatchResult; @@ -62,6 +67,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private boolean useTrailingSlashMatch = true; + private final Map pathPrefixes = new LinkedHashMap<>(); + private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); @Nullable @@ -102,6 +109,22 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi this.useTrailingSlashMatch = useTrailingSlashMatch; } + /** + * Configure path prefixes to apply to controller methods. + *

Prefixes are used to enrich the mappings of every {@code @RequestMapping} + * method whose controller type is matched by the corresponding + * {@link HandlerTypePredicate} in the map. The prefix for the first matching + * predicate is used, assuming the input map has predictable order. + * @param prefixes a map with path prefixes as key + * @since 5.1 + */ + public void setPathPrefixes(Map prefixes) { + this.pathPrefixes.clear(); + prefixes.entrySet().stream() + .filter(entry -> StringUtils.hasText(entry.getKey())) + .forEach(entry -> this.pathPrefixes.put(entry.getKey(), entry.getValue())); + } + /** * Set the {@link ContentNegotiationManager} to use to determine requested media types. * If not set, the default constructor is used. @@ -151,6 +174,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return this.useTrailingSlashMatch; } + /** + * The configured path prefixes as a read-only, possibly empty map. + * @since 5.1 + */ + public Map getPathPrefixes() { + return Collections.unmodifiableMap(this.pathPrefixes); + } + /** * Return the configured {@link ContentNegotiationManager}. */ @@ -195,6 +226,16 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi if (typeInfo != null) { info = typeInfo.combine(info); } + for (Map.Entry entry : this.pathPrefixes.entrySet()) { + if (entry.getValue().test(handlerType)) { + String prefix = entry.getKey(); + if (this.embeddedValueResolver != null) { + prefix = this.embeddedValueResolver.resolveStringValue(prefix); + } + info = RequestMappingInfo.paths(prefix).build().combine(info); + break; + } + } } return info; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java index 929286c4424..475d902e639 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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,7 @@ package org.springframework.web.servlet.config.annotation; +import java.security.Principal; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -48,7 +49,9 @@ import org.springframework.validation.Errors; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; @@ -56,6 +59,8 @@ import org.springframework.web.context.request.async.CallableProcessingIntercept import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -70,6 +75,7 @@ import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; @@ -86,6 +92,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.*; import static org.springframework.http.MediaType.APPLICATION_ATOM_XML; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_XML; @@ -114,6 +121,7 @@ public class WebMvcConfigurationSupportExtensionTests { this.context = new StaticWebApplicationContext(); this.context.setServletContext(new MockServletContext(new FileSystemResourceLoader())); this.context.registerSingleton("controller", TestController.class); + this.context.registerSingleton("userController", UserController.class); this.config = new TestWebMvcConfigurationSupport(); this.config.setApplicationContext(this.context); @@ -135,6 +143,15 @@ public class WebMvcConfigurationSupportExtensionTests { assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass()); assertEquals(ResourceUrlProviderExposingInterceptor.class, chain.getInterceptors()[2].getClass()); + Map map = rmHandlerMapping.getHandlerMethods(); + assertEquals(2, map.size()); + RequestMappingInfo info = map.entrySet().stream() + .filter(entry -> entry.getValue().getBeanType().equals(UserController.class)) + .findFirst() + .orElseThrow(() -> new AssertionError("UserController bean not found")) + .getKey(); + assertEquals(Collections.singleton("/api/user/{id}"), info.getPatternsCondition().getPatterns()); + AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) this.config.viewControllerHandlerMapping(); handlerMapping.setApplicationContext(this.context); assertNotNull(handlerMapping); @@ -402,6 +419,7 @@ public class WebMvcConfigurationSupportExtensionTests { public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setPathMatcher(new TestPathMatcher()); configurer.setUrlPathHelper(new TestPathHelper()); + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); } @Override @@ -453,4 +471,16 @@ public class WebMvcConfigurationSupportExtensionTests { private class TestPathHelper extends UrlPathHelper {} private class TestPathMatcher extends AntPathMatcher {} + + + @RestController + @RequestMapping("/user") + static class UserController { + + @GetMapping("/{id}") + public Principal getUser() { + return mock(Principal.class); + } + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java index fc2d7b8fd92..79d4fc887ad 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -21,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -42,14 +43,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; /** * Tests for {@link RequestMappingHandlerMapping}. @@ -140,6 +140,19 @@ public class RequestMappingHandlerMappingTests { assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result); } + @Test + public void pathPrefix() throws NoSuchMethodException { + this.handlerMapping.setEmbeddedValueResolver(value -> "/${prefix}".equals(value) ? "/api" : value); + this.handlerMapping.setPathPrefixes(Collections.singletonMap( + "/${prefix}", HandlerTypePredicate.forAnnotation(RestController.class))); + + Method method = UserController.class.getMethod("getUser"); + RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, UserController.class); + + assertNotNull(info); + assertEquals(Collections.singleton("/api/user/{id}"), info.getPatternsCondition().getPatterns()); + } + @Test public void resolveRequestMappingViaComposedAnnotation() throws Exception { RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST); @@ -245,6 +258,7 @@ public class RequestMappingHandlerMappingTests { } + @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @@ -256,4 +270,15 @@ public class RequestMappingHandlerMappingTests { String[] value() default {}; } + + @RestController + @RequestMapping("/user") + static class UserController { + + @GetMapping("/{id}") + public Principal getUser() { + return mock(Principal.class); + } + } + } diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 821effc87a4..60818c5b58b 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -2929,16 +2929,8 @@ match to incoming URLs without versions -- e.g. `"/jquery/jquery.min.js"` to === Path Matching [.small]#<># -Spring WebFlux uses parsed representation of path patterns -- i.e. `PathPattern`, and also -the incoming request path -- i.e. `RequestPath`, which eliminates the need to indicate -whether to decode the request path, or remove semicolon content, since `PathPattern` -can now access decoded path segment values and match safely. - -Spring WebFlux also does not support suffix pattern matching so effectively there are only two -minor options to customize related to path matching -- whether to match trailing slashes -(`true` by default) and whether the match is case-sensitive (`false`). - -To customize those options: +Customize options related to path matching. For details on the individual options, see the +{api-spring-framework}/web/reactive/config/PathMatchConfigurer.html[PathMatchConfigurer] Javadoc. [source,java,indent=0] [subs="verbatim,quotes"] @@ -2949,12 +2941,29 @@ To customize those options: @Override public void configurePathMatch(PathMatchConfigurer configurer) { - // ... + configurer + .setUseCaseSensitiveMatch(true) + .setUseTrailingSlashMatch(false) + .addPathPrefix("/api", + HandlerTypePredicate.forAnnotation(RestController.class)); } } ---- +[TIP] +==== +Spring WebFlux relies on a parsed representation of the request path called +`RequestPath` for access to decoded path segment values, with semicolon content removed +(i.e. path/matrix variables). That means, unlike Spring MVC, there is no need to indicate +neither whether to decode the request path, nor whether to remove semicolon content for +path matching purposes. + +Spring WebFlux also does not support suffix pattern matching, unlike Spring MVC, where we +also <> moving away from +reliance on it. +==== + [[webflux-config-advanced-java]] diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index e90224c0b9e..eef190f9364 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -4644,9 +4644,9 @@ Or in XML: === Path Matching [.small]#<># -This allows customizing options related to URL matching and treatment of the URL. -For details on the individual options check out the -{api-spring-framework}/web/servlet/config/annotation/PathMatchConfigurer.html[PathMatchConfigurer] API. +Customize options related to path matching, and treatment of the URL. +For details on the individual options, see the +{api-spring-framework}/web/servlet/config/annotation/PathMatchConfigurer.html[PathMatchConfigurer] Javadoc. Example in Java config: @@ -4664,7 +4664,9 @@ Example in Java config: .setUseTrailingSlashMatch(false) .setUseRegisteredSuffixPatternMatch(true) .setPathMatcher(antPathMatcher()) - .setUrlPathHelper(urlPathHelper()); + .setUrlPathHelper(urlPathHelper()) + .addPathPrefix("/api", + HandlerTypePredicate.forAnnotation(RestController.class)); } @Bean