From 1cd0f433e0a5dd9b659143664ea432d24b4386eb Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 23 Apr 2015 10:59:38 -0400 Subject: [PATCH] Allow MvcUriComponentsBuilder instance with baseUrl Before this change MvcUriComponentsBuilder exposed only static factory methods for creating links where the links are relative to the current request or a baseUrl explicitly provided as an argument. This change allows creating an MvcUriComponents builder instance with a built-in baseUrl. The instance can then be used with non-static withXxx(...) method alternatives to the static fromXxx(...) methods. Issue: SPR-12617 --- .../annotation/MvcUriComponentsBuilder.java | 102 ++++++++++++++++-- ...mponentsBuilderMethodArgumentResolver.java | 7 +- .../MvcUriComponentsBuilderTests.java | 86 ++++++++++++++- 3 files changed, 184 insertions(+), 11 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index 058cbb126d..7b8a7c271f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -64,8 +64,19 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** - * A UriComponentsBuilder that helps to build URIs to Spring MVC controllers - * and methods from their request mappings. + * Creates instances of {@link org.springframework.web.util.UriComponentsBuilder} + * by pointing to Spring MVC controllers and {@code @RequestMapping} methods. + * + *

The static {@code fromXxx(...)} methods prepare links relative to the + * current request as determined by a call to + * {@link org.springframework.web.servlet.support.ServletUriComponentsBuilder#fromCurrentServletMapping()}. + * + *

The static {@code fromXxx(UriComponentsBuilder,...)} methods can be given + * the baseUrl when operating outside the context of a request. + * + *

You can also create an MvcUriComponentsBuilder instance with a baseUrl + * via {@link #relativeTo(org.springframework.web.util.UriComponentsBuilder)} + * and then use the non-static {@code withXxx(...)} method variants. * * @author Oliver Gierke * @author Rossen Stoyanchev @@ -94,6 +105,8 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { new PathVariableMethodArgumentResolver(), new RequestParamMethodArgumentResolver(false)); } + private final UriComponentsBuilder baseUrl; + /** * Default constructor. Protected to prevent direct instantiation. @@ -104,7 +117,9 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * @see #fromMappingName(String) * @see #fromMethod(java.lang.reflect.Method, Object...) */ - protected MvcUriComponentsBuilder() { + protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) { + Assert.notNull(baseUrl, "'baseUrl' is required"); + this.baseUrl = baseUrl; } /** @@ -113,8 +128,19 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { */ protected MvcUriComponentsBuilder(MvcUriComponentsBuilder other) { super(other); + this.baseUrl = other.baseUrl; } + /** + * Create an instance of this class with a base URL. After that calls to one + * of the instance based {@code withXxx(...}} methods will create URLs relative + * to the given base URL. + */ + public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { + return new MvcUriComponentsBuilder(baseUrl); + } + + /** * Create a {@link UriComponentsBuilder} from the mapping of a controller class * and current request information including Servlet mapping. If the controller @@ -316,6 +342,23 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * @since 4.1 */ public static MethodArgumentBuilder fromMappingName(String mappingName) { + return fromMappingName(null, mappingName); + } + + /** + * An alternative to {@link #fromMappingName(String)} that accepts a + * {@code UriComponentsBuilder} representing the base URL. This is useful + * when using MvcUriComponentsBuilder outside the context of processing a + * request or to apply a custom baseUrl not matching the current request. + * @param builder the builder for the base URL; the builder will be cloned + * and therefore not modified and may be re-used for further calls. + * @param mappingName the mapping name + * @return a builder to to prepare the URI String + * @throws IllegalArgumentException if the mapping name is not found or + * if there is no unique match + * @since 4.2 + */ + public static MethodArgumentBuilder fromMappingName(UriComponentsBuilder builder, String mappingName) { RequestMappingInfoHandlerMapping handlerMapping = getRequestMappingInfoHandlerMapping(); List handlerMethods = handlerMapping.getHandlerMethodsForMappingName(mappingName); if (handlerMethods == null) { @@ -325,7 +368,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { throw new IllegalArgumentException( "No unique match for mapping mappingName " + mappingName + ": " + handlerMethods); } - return new MethodArgumentBuilder(handlerMethods.get(0).getMethod()); + return new MethodArgumentBuilder(builder, handlerMethods.get(0).getMethod()); } /** @@ -530,6 +573,46 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { } } + /** + * An alternative to {@link #fromController(Class)} for use with an instance + * of this class created via a call to {@link #relativeTo}. + */ + public UriComponentsBuilder withController(Class controllerType) { + return fromController(this.baseUrl, controllerType); + } + + /** + * An alternative to {@link #fromMethodName(Class, String, Object...)}} for + * use with an instance of this class created via {@link #relativeTo}. + */ + public UriComponentsBuilder withMethodName(Class controllerType, String methodName, Object... args) { + return fromMethodName(this.baseUrl, controllerType, methodName, args); + } + + /** + * An alternative to {@link #fromMethodCall(Object)} for use with an instance + * of this class created via {@link #relativeTo}. + */ + public UriComponentsBuilder withMethodCall(Object invocationInfo) { + return fromMethodCall(this.baseUrl, invocationInfo); + } + + /** + * An alternative to {@link #fromMappingName(String)} for use with an instance + * of this class created via {@link #relativeTo}. + */ + public MethodArgumentBuilder withMappingName(String mappingName) { + return fromMappingName(this.baseUrl, mappingName); + } + + /** + * An alternative to {@link #fromMethod(java.lang.reflect.Method, Object...)} + * for use with an instance of this class created via {@link #relativeTo}. + */ + public UriComponentsBuilder withMethod(Method method, Object... args) { + return fromMethod(this.baseUrl, method, args); + } + @Override public Object clone() { return new MvcUriComponentsBuilder(this); @@ -590,9 +673,16 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { private final Object[] argumentValues; + private final UriComponentsBuilder baseUrl; + public MethodArgumentBuilder(Method method) { + this(null, method); + } + + public MethodArgumentBuilder(UriComponentsBuilder baseUrl, Method method) { Assert.notNull(method, "'method' is required"); + this.baseUrl = baseUrl; this.method = method; this.argumentValues = new Object[method.getParameterTypes().length]; for (int i = 0; i < this.argumentValues.length; i++) { @@ -606,12 +696,12 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { } public String build() { - return MvcUriComponentsBuilder.fromMethod(this.method, this.argumentValues) + return MvcUriComponentsBuilder.fromMethod(this.baseUrl, this.method, this.argumentValues) .build(false).encode().toUriString(); } public String buildAndExpand(Object... uriVariables) { - return MvcUriComponentsBuilder.fromMethod(this.method, this.argumentValues) + return MvcUriComponentsBuilder.fromMethod(this.baseUrl, this.method, this.argumentValues) .build(false).expand(uriVariables).encode().toString(); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java index 74e632f2a9..d01f2e8cb7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-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,6 +26,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; + /** * Resolvers argument values of type {@link UriComponentsBuilder}. * @@ -37,9 +38,11 @@ import org.springframework.web.util.UriComponentsBuilder; */ public class UriComponentsBuilderMethodArgumentResolver implements HandlerMethodArgumentResolver { + @Override public boolean supportsParameter(MethodParameter parameter) { - return UriComponentsBuilder.class.isAssignableFrom(parameter.getParameterType()); + Class type = parameter.getParameterType(); + return (UriComponentsBuilder.class.equals(type) || ServletUriComponentsBuilder.class.equals(type)); } @Override diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java index 416c90501c..e2b7d3c373 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java @@ -30,10 +30,12 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.springframework.context.annotation.Bean; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.http.HttpEntity; import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockServletContext; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -41,6 +43,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -100,7 +106,7 @@ public class MvcUriComponentsBuilderTests { } @Test - public void testFromControllerWithCustomBaseUrl() { + public void testFromControllerWithCustomBaseUrlViaStaticCall() { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.org:9090/base"); UriComponents uriComponents = fromController(builder, PersonControllerImpl.class).build(); @@ -108,6 +114,16 @@ public class MvcUriComponentsBuilderTests { assertEquals("http://example.org:9090/base", builder.toUriString()); } + @Test + public void testFromControllerWithCustomBaseUrlViaInstance() { + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.org:9090/base"); + MvcUriComponentsBuilder mvcBuilder = MvcUriComponentsBuilder.relativeTo(builder); + UriComponents uriComponents = mvcBuilder.withController(PersonControllerImpl.class).build(); + + assertEquals("http://example.org:9090/base/people", uriComponents.toString()); + assertEquals("http://example.org:9090/base", builder.toUriString()); + } + @Test public void testFromMethodNamePathVariable() throws Exception { UriComponents uriComponents = fromMethodName( @@ -162,7 +178,7 @@ public class MvcUriComponentsBuilderTests { } @Test - public void testFromMethodNameWithCustomBaseUrl() throws Exception { + public void testFromMethodNameWithCustomBaseUrlViaStaticCall() throws Exception { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.org:9090/base"); UriComponents uriComponents = fromMethodName(builder, ControllerWithMethods.class, "methodWithPathVariable", new Object[] {"1"}).build(); @@ -171,6 +187,17 @@ public class MvcUriComponentsBuilderTests { assertEquals("http://example.org:9090/base", builder.toUriString()); } + @Test + public void testFromMethodNameWithCustomBaseUrlViaInstance() throws Exception { + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.org:9090/base"); + MvcUriComponentsBuilder mvcBuilder = MvcUriComponentsBuilder.relativeTo(builder); + UriComponents uriComponents = mvcBuilder.withMethodName(ControllerWithMethods.class, + "methodWithPathVariable", new Object[] {"1"}).build(); + + assertEquals("http://example.org:9090/base/something/1/foo", uriComponents.toString()); + assertEquals("http://example.org:9090/base", builder.toUriString()); + } + @Test public void testFromMethodCall() { UriComponents uriComponents = fromMethodCall(on(ControllerWithMethods.class).myMethod(null)).build(); @@ -222,7 +249,7 @@ public class MvcUriComponentsBuilderTests { } @Test - public void testFromMethodCallWithCustomBaseUrl() { + public void testFromMethodCallWithCustomBaseUrlViaStaticCall() { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.org:9090/base"); UriComponents uriComponents = fromMethodCall(builder, on(ControllerWithMethods.class).myMethod(null)).build(); @@ -230,6 +257,48 @@ public class MvcUriComponentsBuilderTests { assertEquals("http://example.org:9090/base", builder.toUriString()); } + @Test + public void testFromMethodCallWithCustomBaseUrlViaInstance() { + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("http://example.org:9090/base"); + MvcUriComponentsBuilder mvcBuilder = MvcUriComponentsBuilder.relativeTo(builder); + UriComponents result = mvcBuilder.withMethodCall(on(ControllerWithMethods.class).myMethod(null)).build(); + + assertEquals("http://example.org:9090/base/something/else", result.toString()); + assertEquals("http://example.org:9090/base", builder.toUriString()); + } + + @Test + public void testFromMappingName() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + context.register(WebConfig.class); + context.refresh(); + + this.request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); + this.request.setServerName("example.org"); + this.request.setServerPort(9999); + this.request.setContextPath("/base"); + + String mappingName = "PAC#getAddressesForCountry"; + String url = MvcUriComponentsBuilder.fromMappingName(mappingName).arg(0, "DE").buildAndExpand(123); + assertEquals("http://example.org:9999/base/people/123/addresses/DE", url); + } + + @Test + public void testFromMappingNameWithCustomBaseUrl() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + context.register(WebConfig.class); + context.refresh(); + + this.request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); + + UriComponentsBuilder baseUrl = UriComponentsBuilder.fromUriString("http://example.org:9999/base"); + MvcUriComponentsBuilder mvcBuilder = MvcUriComponentsBuilder.relativeTo(baseUrl); + String url = mvcBuilder.withMappingName("PAC#getAddressesForCountry").arg(0, "DE").buildAndExpand(123); + assertEquals("http://example.org:9999/base/people/123/addresses/DE", url); + } + @Test public void usesForwardedHostAsHostIfHeaderIsSet() { this.request.addHeader("X-Forwarded-Host", "somethingDifferent"); @@ -329,6 +398,7 @@ public class MvcUriComponentsBuilderTests { } } + @SuppressWarnings("unused") @RequestMapping("/user/{userId}/contacts") static class UserContactController { @@ -338,4 +408,14 @@ public class MvcUriComponentsBuilderTests { } } + @EnableWebMvc + static class WebConfig extends WebMvcConfigurerAdapter { + + @Bean + public PersonsAddressesController controller() { + return new PersonsAddressesController(); + } + + } + }