diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index 82f366e2c4..008a6c3358 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -20,6 +20,7 @@ import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -35,6 +36,7 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.util.HierarchicalUriComponents.PathComponent; +import org.springframework.web.util.UriComponents.UriTemplateVariables; /** * Builder for {@link UriComponents}. @@ -121,6 +123,8 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { @Nullable private String fragment; + private final Map uriVariables = new HashMap<>(4); + private boolean encodeTemplate; private Charset charset = StandardCharsets.UTF_8; @@ -381,15 +385,20 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { * @return the URI components */ public UriComponents build(boolean encoded) { + UriComponents result; if (this.ssp != null) { - return new OpaqueUriComponents(this.scheme, this.ssp, this.fragment); + result = new OpaqueUriComponents(this.scheme, this.ssp, this.fragment); } else { - HierarchicalUriComponents uriComponents = - new HierarchicalUriComponents(this.scheme, this.fragment, this.userInfo, - this.host, this.port, this.pathBuilder.build(), this.queryParams, encoded); - return (this.encodeTemplate ? uriComponents.encodeTemplate(this.charset) : uriComponents); + HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment, + this.userInfo, this.host, this.port, this.pathBuilder.build(), this.queryParams, encoded); + + result = this.encodeTemplate ? uric.encodeTemplate(this.charset) : uric; } + if (!this.uriVariables.isEmpty()) { + result = result.expand(name -> this.uriVariables.getOrDefault(name, UriTemplateVariables.SKIP_VALUE)); + } + return result; } /** @@ -433,7 +442,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { * @see UriComponents#toUriString() */ public String toUriString() { - return build().encode().toUriString(); + return encode().build().toUriString(); } @@ -752,6 +761,25 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { return this; } + /** + * Configure URI variables to be expanded at build time. + *

The provided variables may be a subset of all required ones. At build + * time, the available ones are expanded, while unresolved URI placeholders + * are left in place and can still be expanded later. + *

In contrast to {@link UriComponents#expand(Map)} or + * {@link #buildAndExpand(Map)}, this method is useful when you need to + * supply URI variables without building the {@link UriComponents} instance + * just yet, or perhaps pre-expand some shared default values such as host + * and port. + * @param uriVariables the URI variables to use + * @return this UriComponentsBuilder + * @since 5.0.8 + */ + public UriComponentsBuilder uriVariables(Map uriVariables) { + this.uriVariables.putAll(uriVariables); + return this; + } + /** * Adapt this builder's scheme+host+port from the given headers, specifically * "Forwarded" (RFC 7239, diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java index 9ee01d0086..480b18e865 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsTests.java @@ -70,19 +70,12 @@ public class UriComponentsTests { @Test public void encodeAndExpandPartially() { - Map uriVars = new HashMap<>(); - uriVars.put("city", "Z\u00fcrich"); - UriComponents uri = UriComponentsBuilder - .fromPath("/hotel list/{city} specials").queryParam("q", "{value}").encode().build() - .expand(name -> uriVars.getOrDefault(name, UriTemplateVariables.SKIP_VALUE)); + .fromPath("/hotel list/{city} specials").queryParam("q", "{value}").encode() + .uriVariables(Collections.singletonMap("city", "Z\u00fcrich")) + .build(); - assertEquals("/hotel%20list/Z%C3%BCrich%20specials?q={value}", uri.toString()); - - uriVars.put("value", "a+b"); - uri = uri.expand(uriVars); - - assertEquals("/hotel%20list/Z%C3%BCrich%20specials?q=a%2Bb", uri.toString()); + assertEquals("/hotel%20list/Z%C3%BCrich%20specials?q=a%2Bb", uri.expand("a+b").toString()); } @Test 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 c11dd23cfb..844c61df01 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 @@ -63,7 +63,6 @@ import org.springframework.web.method.support.CompositeUriComponentsContributor; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -547,8 +546,7 @@ public class MvcUriComponentsBuilder { String path = pathMatcher.combine(typePath, methodPath); builder.path(path); - UriComponents uriComponents = applyContributors(builder, method, args); - return UriComponentsBuilder.newInstance().uriComponents(uriComponents); + return applyContributors(builder, method, args); } private static UriComponentsBuilder getBaseUrlToUse(@Nullable UriComponentsBuilder baseUrl) { @@ -626,7 +624,7 @@ public class MvcUriComponentsBuilder { } } - private static UriComponents applyContributors(UriComponentsBuilder builder, Method method, Object... args) { + private static UriComponentsBuilder applyContributors(UriComponentsBuilder builder, Method method, Object... args) { CompositeUriComponentsContributor contributor = getUriComponentsContributor(); int paramCount = method.getParameterCount(); @@ -643,9 +641,8 @@ public class MvcUriComponentsBuilder { contributor.contributeMethodArgument(param, args[i], builder, uriVars); } - // We may not have all URI var values, expand only what we have - return builder.build().expand(name -> - uriVars.getOrDefault(name, UriComponents.UriTemplateVariables.SKIP_VALUE)); + // This may not be all the URI variables, supply what we have so far.. + return builder.uriVariables(uriVars); } private static CompositeUriComponentsContributor getUriComponentsContributor() { @@ -851,7 +848,7 @@ public class MvcUriComponentsBuilder { public MethodArgumentBuilder(@Nullable UriComponentsBuilder baseUrl, Class controllerType, Method method) { Assert.notNull(controllerType, "'controllerType' is required"); Assert.notNull(method, "'method' is required"); - this.baseUrl = (baseUrl != null ? baseUrl : initBaseUrl()); + this.baseUrl = baseUrl != null ? baseUrl : UriComponentsBuilder.fromPath(getPath()); this.controllerType = controllerType; this.method = method; this.argumentValues = new Object[method.getParameterCount()]; @@ -860,10 +857,10 @@ public class MvcUriComponentsBuilder { } } - private static UriComponentsBuilder initBaseUrl() { + private static String getPath() { UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping(); String path = builder.build().getPath(); - return (path != null ? UriComponentsBuilder.fromPath(path) : UriComponentsBuilder.newInstance()); + return path != null ? path : ""; } public MethodArgumentBuilder arg(int index, Object value) { @@ -872,13 +869,13 @@ public class MvcUriComponentsBuilder { } public String build() { - return fromMethodInternal(this.baseUrl, this.controllerType, this.method, - this.argumentValues).build(false).encode().toUriString(); + return fromMethodInternal(this.baseUrl, this.controllerType, this.method, this.argumentValues) + .build(false).encode().toUriString(); } public String buildAndExpand(Object... uriVars) { - return fromMethodInternal(this.baseUrl, this.controllerType, this.method, - this.argumentValues).build(false).expand(uriVars).encode().toString(); + return fromMethodInternal(this.baseUrl, this.controllerType, this.method, this.argumentValues) + .build(false).expand(uriVars).encode().toString(); } }