001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.util; 018 019import java.net.URI; 020import java.nio.charset.Charset; 021import java.nio.charset.StandardCharsets; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.LinkedList; 026import java.util.List; 027import java.util.Map; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031import org.springframework.http.HttpHeaders; 032import org.springframework.http.HttpRequest; 033import org.springframework.lang.Nullable; 034import org.springframework.util.Assert; 035import org.springframework.util.CollectionUtils; 036import org.springframework.util.LinkedMultiValueMap; 037import org.springframework.util.MultiValueMap; 038import org.springframework.util.ObjectUtils; 039import org.springframework.util.StringUtils; 040import org.springframework.web.util.HierarchicalUriComponents.PathComponent; 041import org.springframework.web.util.UriComponents.UriTemplateVariables; 042 043/** 044 * Builder for {@link UriComponents}. 045 * 046 * <p>Typical usage involves: 047 * <ol> 048 * <li>Create a {@code UriComponentsBuilder} with one of the static factory methods 049 * (such as {@link #fromPath(String)} or {@link #fromUri(URI)})</li> 050 * <li>Set the various URI components through the respective methods ({@link #scheme(String)}, 051 * {@link #userInfo(String)}, {@link #host(String)}, {@link #port(int)}, {@link #path(String)}, 052 * {@link #pathSegment(String...)}, {@link #queryParam(String, Object...)}, and 053 * {@link #fragment(String)}.</li> 054 * <li>Build the {@link UriComponents} instance with the {@link #build()} method.</li> 055 * </ol> 056 * 057 * @author Arjen Poutsma 058 * @author Rossen Stoyanchev 059 * @author Phillip Webb 060 * @author Oliver Gierke 061 * @author Brian Clozel 062 * @author Sebastien Deleuze 063 * @author Sam Brannen 064 * @since 3.1 065 * @see #newInstance() 066 * @see #fromPath(String) 067 * @see #fromUri(URI) 068 */ 069public class UriComponentsBuilder implements UriBuilder, Cloneable { 070 071 private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); 072 073 private static final String SCHEME_PATTERN = "([^:/?#]+):"; 074 075 private static final String HTTP_PATTERN = "(?i)(http|https):"; 076 077 private static final String USERINFO_PATTERN = "([^@\\[/?#]*)"; 078 079 private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*"; 080 081 private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}:.]*[%\\p{Alnum}]*]"; 082 083 private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; 084 085 private static final String PORT_PATTERN = "(\\d*(?:\\{[^/]+?})?)"; 086 087 private static final String PATH_PATTERN = "([^?#]*)"; 088 089 private static final String QUERY_PATTERN = "([^#]*)"; 090 091 private static final String LAST_PATTERN = "(.*)"; 092 093 // Regex patterns that matches URIs. See RFC 3986, appendix B 094 private static final Pattern URI_PATTERN = Pattern.compile( 095 "^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + 096 ")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?"); 097 098 private static final Pattern HTTP_URL_PATTERN = Pattern.compile( 099 "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" + 100 PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?"); 101 102 private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?"); 103 104 private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?"); 105 106 private static final Object[] EMPTY_VALUES = new Object[0]; 107 108 109 @Nullable 110 private String scheme; 111 112 @Nullable 113 private String ssp; 114 115 @Nullable 116 private String userInfo; 117 118 @Nullable 119 private String host; 120 121 @Nullable 122 private String port; 123 124 private CompositePathComponentBuilder pathBuilder; 125 126 private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); 127 128 @Nullable 129 private String fragment; 130 131 private final Map<String, Object> uriVariables = new HashMap<>(4); 132 133 private boolean encodeTemplate; 134 135 private Charset charset = StandardCharsets.UTF_8; 136 137 138 /** 139 * Default constructor. Protected to prevent direct instantiation. 140 * @see #newInstance() 141 * @see #fromPath(String) 142 * @see #fromUri(URI) 143 */ 144 protected UriComponentsBuilder() { 145 this.pathBuilder = new CompositePathComponentBuilder(); 146 } 147 148 /** 149 * Create a deep copy of the given UriComponentsBuilder. 150 * @param other the other builder to copy from 151 * @since 4.1.3 152 */ 153 protected UriComponentsBuilder(UriComponentsBuilder other) { 154 this.scheme = other.scheme; 155 this.ssp = other.ssp; 156 this.userInfo = other.userInfo; 157 this.host = other.host; 158 this.port = other.port; 159 this.pathBuilder = other.pathBuilder.cloneBuilder(); 160 this.uriVariables.putAll(other.uriVariables); 161 this.queryParams.addAll(other.queryParams); 162 this.fragment = other.fragment; 163 this.encodeTemplate = other.encodeTemplate; 164 this.charset = other.charset; 165 } 166 167 168 // Factory methods 169 170 /** 171 * Create a new, empty builder. 172 * @return the new {@code UriComponentsBuilder} 173 */ 174 public static UriComponentsBuilder newInstance() { 175 return new UriComponentsBuilder(); 176 } 177 178 /** 179 * Create a builder that is initialized with the given path. 180 * @param path the path to initialize with 181 * @return the new {@code UriComponentsBuilder} 182 */ 183 public static UriComponentsBuilder fromPath(String path) { 184 UriComponentsBuilder builder = new UriComponentsBuilder(); 185 builder.path(path); 186 return builder; 187 } 188 189 /** 190 * Create a builder that is initialized from the given {@code URI}. 191 * <p><strong>Note:</strong> the components in the resulting builder will be 192 * in fully encoded (raw) form and further changes must also supply values 193 * that are fully encoded, for example via methods in {@link UriUtils}. 194 * In addition please use {@link #build(boolean)} with a value of "true" to 195 * build the {@link UriComponents} instance in order to indicate that the 196 * components are encoded. 197 * @param uri the URI to initialize with 198 * @return the new {@code UriComponentsBuilder} 199 */ 200 public static UriComponentsBuilder fromUri(URI uri) { 201 UriComponentsBuilder builder = new UriComponentsBuilder(); 202 builder.uri(uri); 203 return builder; 204 } 205 206 /** 207 * Create a builder that is initialized with the given URI string. 208 * <p><strong>Note:</strong> The presence of reserved characters can prevent 209 * correct parsing of the URI string. For example if a query parameter 210 * contains {@code '='} or {@code '&'} characters, the query string cannot 211 * be parsed unambiguously. Such values should be substituted for URI 212 * variables to enable correct parsing: 213 * <pre class="code"> 214 * String uriString = "/hotels/42?filter={value}"; 215 * UriComponentsBuilder.fromUriString(uriString).buildAndExpand("hot&cold"); 216 * </pre> 217 * @param uri the URI string to initialize with 218 * @return the new {@code UriComponentsBuilder} 219 */ 220 public static UriComponentsBuilder fromUriString(String uri) { 221 Assert.notNull(uri, "URI must not be null"); 222 Matcher matcher = URI_PATTERN.matcher(uri); 223 if (matcher.matches()) { 224 UriComponentsBuilder builder = new UriComponentsBuilder(); 225 String scheme = matcher.group(2); 226 String userInfo = matcher.group(5); 227 String host = matcher.group(6); 228 String port = matcher.group(8); 229 String path = matcher.group(9); 230 String query = matcher.group(11); 231 String fragment = matcher.group(13); 232 boolean opaque = false; 233 if (StringUtils.hasLength(scheme)) { 234 String rest = uri.substring(scheme.length()); 235 if (!rest.startsWith(":/")) { 236 opaque = true; 237 } 238 } 239 builder.scheme(scheme); 240 if (opaque) { 241 String ssp = uri.substring(scheme.length() + 1); 242 if (StringUtils.hasLength(fragment)) { 243 ssp = ssp.substring(0, ssp.length() - (fragment.length() + 1)); 244 } 245 builder.schemeSpecificPart(ssp); 246 } 247 else { 248 builder.userInfo(userInfo); 249 builder.host(host); 250 if (StringUtils.hasLength(port)) { 251 builder.port(port); 252 } 253 builder.path(path); 254 builder.query(query); 255 } 256 if (StringUtils.hasText(fragment)) { 257 builder.fragment(fragment); 258 } 259 return builder; 260 } 261 else { 262 throw new IllegalArgumentException("[" + uri + "] is not a valid URI"); 263 } 264 } 265 266 /** 267 * Create a URI components builder from the given HTTP URL String. 268 * <p><strong>Note:</strong> The presence of reserved characters can prevent 269 * correct parsing of the URI string. For example if a query parameter 270 * contains {@code '='} or {@code '&'} characters, the query string cannot 271 * be parsed unambiguously. Such values should be substituted for URI 272 * variables to enable correct parsing: 273 * <pre class="code"> 274 * String urlString = "https://example.com/hotels/42?filter={value}"; 275 * UriComponentsBuilder.fromHttpUrl(urlString).buildAndExpand("hot&cold"); 276 * </pre> 277 * @param httpUrl the source URI 278 * @return the URI components of the URI 279 */ 280 public static UriComponentsBuilder fromHttpUrl(String httpUrl) { 281 Assert.notNull(httpUrl, "HTTP URL must not be null"); 282 Matcher matcher = HTTP_URL_PATTERN.matcher(httpUrl); 283 if (matcher.matches()) { 284 UriComponentsBuilder builder = new UriComponentsBuilder(); 285 String scheme = matcher.group(1); 286 builder.scheme(scheme != null ? scheme.toLowerCase() : null); 287 builder.userInfo(matcher.group(4)); 288 String host = matcher.group(5); 289 if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) { 290 throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); 291 } 292 builder.host(host); 293 String port = matcher.group(7); 294 if (StringUtils.hasLength(port)) { 295 builder.port(port); 296 } 297 builder.path(matcher.group(8)); 298 builder.query(matcher.group(10)); 299 String fragment = matcher.group(12); 300 if (StringUtils.hasText(fragment)) { 301 builder.fragment(fragment); 302 } 303 return builder; 304 } 305 else { 306 throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); 307 } 308 } 309 310 /** 311 * Create a new {@code UriComponents} object from the URI associated with 312 * the given HttpRequest while also overlaying with values from the headers 313 * "Forwarded" (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>), 314 * or "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if 315 * "Forwarded" is not found. 316 * @param request the source request 317 * @return the URI components of the URI 318 * @since 4.1.5 319 */ 320 public static UriComponentsBuilder fromHttpRequest(HttpRequest request) { 321 return fromUri(request.getURI()).adaptFromForwardedHeaders(request.getHeaders()); 322 } 323 324 /** 325 * Create an instance by parsing the "Origin" header of an HTTP request. 326 * @see <a href="https://tools.ietf.org/html/rfc6454">RFC 6454</a> 327 */ 328 public static UriComponentsBuilder fromOriginHeader(String origin) { 329 Matcher matcher = URI_PATTERN.matcher(origin); 330 if (matcher.matches()) { 331 UriComponentsBuilder builder = new UriComponentsBuilder(); 332 String scheme = matcher.group(2); 333 String host = matcher.group(6); 334 String port = matcher.group(8); 335 if (StringUtils.hasLength(scheme)) { 336 builder.scheme(scheme); 337 } 338 builder.host(host); 339 if (StringUtils.hasLength(port)) { 340 builder.port(port); 341 } 342 return builder; 343 } 344 else { 345 throw new IllegalArgumentException("[" + origin + "] is not a valid \"Origin\" header value"); 346 } 347 } 348 349 350 // Encode methods 351 352 /** 353 * Request to have the URI template pre-encoded at build time, and 354 * URI variables encoded separately when expanded. 355 * <p>In comparison to {@link UriComponents#encode()}, this method has the 356 * same effect on the URI template, i.e. each URI component is encoded by 357 * replacing non-ASCII and illegal (within the URI component type) characters 358 * with escaped octets. However URI variables are encoded more strictly, by 359 * also escaping characters with reserved meaning. 360 * <p>For most cases, this method is more likely to give the expected result 361 * because in treats URI variables as opaque data to be fully encoded, while 362 * {@link UriComponents#encode()} is useful only if intentionally expanding 363 * URI variables that contain reserved characters. 364 * <p>For example ';' is legal in a path but has reserved meaning. This 365 * method replaces ";" with "%3B" in URI variables but not in the URI 366 * template. By contrast, {@link UriComponents#encode()} never replaces ";" 367 * since it is a legal character in a path. 368 * @since 5.0.8 369 */ 370 public final UriComponentsBuilder encode() { 371 return encode(StandardCharsets.UTF_8); 372 } 373 374 /** 375 * A variant of {@link #encode()} with a charset other than "UTF-8". 376 * @param charset the charset to use for encoding 377 * @since 5.0.8 378 */ 379 public UriComponentsBuilder encode(Charset charset) { 380 this.encodeTemplate = true; 381 this.charset = charset; 382 return this; 383 } 384 385 386 // Build methods 387 388 /** 389 * Build a {@code UriComponents} instance from the various components contained in this builder. 390 * @return the URI components 391 */ 392 public UriComponents build() { 393 return build(false); 394 } 395 396 /** 397 * Variant of {@link #build()} to create a {@link UriComponents} instance 398 * when components are already fully encoded. This is useful for example if 399 * the builder was created via {@link UriComponentsBuilder#fromUri(URI)}. 400 * @param encoded whether the components in this builder are already encoded 401 * @return the URI components 402 * @throws IllegalArgumentException if any of the components contain illegal 403 * characters that should have been encoded. 404 */ 405 public UriComponents build(boolean encoded) { 406 return buildInternal(encoded ? EncodingHint.FULLY_ENCODED : 407 (this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE)); 408 } 409 410 private UriComponents buildInternal(EncodingHint hint) { 411 UriComponents result; 412 if (this.ssp != null) { 413 result = new OpaqueUriComponents(this.scheme, this.ssp, this.fragment); 414 } 415 else { 416 HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment, 417 this.userInfo, this.host, this.port, this.pathBuilder.build(), this.queryParams, 418 hint == EncodingHint.FULLY_ENCODED); 419 result = (hint == EncodingHint.ENCODE_TEMPLATE ? uric.encodeTemplate(this.charset) : uric); 420 } 421 if (!this.uriVariables.isEmpty()) { 422 result = result.expand(name -> this.uriVariables.getOrDefault(name, UriTemplateVariables.SKIP_VALUE)); 423 } 424 return result; 425 } 426 427 /** 428 * Build a {@code UriComponents} instance and replaces URI template variables 429 * with the values from a map. This is a shortcut method which combines 430 * calls to {@link #build()} and then {@link UriComponents#expand(Map)}. 431 * @param uriVariables the map of URI variables 432 * @return the URI components with expanded values 433 */ 434 public UriComponents buildAndExpand(Map<String, ?> uriVariables) { 435 return build().expand(uriVariables); 436 } 437 438 /** 439 * Build a {@code UriComponents} instance and replaces URI template variables 440 * with the values from an array. This is a shortcut method which combines 441 * calls to {@link #build()} and then {@link UriComponents#expand(Object...)}. 442 * @param uriVariableValues the URI variable values 443 * @return the URI components with expanded values 444 */ 445 public UriComponents buildAndExpand(Object... uriVariableValues) { 446 return build().expand(uriVariableValues); 447 } 448 449 @Override 450 public URI build(Object... uriVariables) { 451 return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri(); 452 } 453 454 @Override 455 public URI build(Map<String, ?> uriVariables) { 456 return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri(); 457 } 458 459 /** 460 * Build a URI String. 461 * <p>Effectively, a shortcut for building, encoding, and returning the 462 * String representation: 463 * <pre class="code"> 464 * String uri = builder.build().encode().toUriString() 465 * </pre> 466 * <p>However if {@link #uriVariables(Map) URI variables} have been provided 467 * then the URI template is pre-encoded separately from URI variables (see 468 * {@link #encode()} for details), i.e. equivalent to: 469 * <pre> 470 * String uri = builder.encode().build().toUriString() 471 * </pre> 472 * @since 4.1 473 * @see UriComponents#toUriString() 474 */ 475 public String toUriString() { 476 return (this.uriVariables.isEmpty() ? build().encode().toUriString() : 477 buildInternal(EncodingHint.ENCODE_TEMPLATE).toUriString()); 478 } 479 480 481 // Instance methods 482 483 /** 484 * Initialize components of this builder from components of the given URI. 485 * @param uri the URI 486 * @return this UriComponentsBuilder 487 */ 488 public UriComponentsBuilder uri(URI uri) { 489 Assert.notNull(uri, "URI must not be null"); 490 this.scheme = uri.getScheme(); 491 if (uri.isOpaque()) { 492 this.ssp = uri.getRawSchemeSpecificPart(); 493 resetHierarchicalComponents(); 494 } 495 else { 496 if (uri.getRawUserInfo() != null) { 497 this.userInfo = uri.getRawUserInfo(); 498 } 499 if (uri.getHost() != null) { 500 this.host = uri.getHost(); 501 } 502 if (uri.getPort() != -1) { 503 this.port = String.valueOf(uri.getPort()); 504 } 505 if (StringUtils.hasLength(uri.getRawPath())) { 506 this.pathBuilder = new CompositePathComponentBuilder(); 507 this.pathBuilder.addPath(uri.getRawPath()); 508 } 509 if (StringUtils.hasLength(uri.getRawQuery())) { 510 this.queryParams.clear(); 511 query(uri.getRawQuery()); 512 } 513 resetSchemeSpecificPart(); 514 } 515 if (uri.getRawFragment() != null) { 516 this.fragment = uri.getRawFragment(); 517 } 518 return this; 519 } 520 521 /** 522 * Set or append individual URI components of this builder from the values 523 * of the given {@link UriComponents} instance. 524 * <p>For the semantics of each component (i.e. set vs append) check the 525 * builder methods on this class. For example {@link #host(String)} sets 526 * while {@link #path(String)} appends. 527 * @param uriComponents the UriComponents to copy from 528 * @return this UriComponentsBuilder 529 */ 530 public UriComponentsBuilder uriComponents(UriComponents uriComponents) { 531 Assert.notNull(uriComponents, "UriComponents must not be null"); 532 uriComponents.copyToUriComponentsBuilder(this); 533 return this; 534 } 535 536 @Override 537 public UriComponentsBuilder scheme(@Nullable String scheme) { 538 this.scheme = scheme; 539 return this; 540 } 541 542 /** 543 * Set the URI scheme-specific-part. When invoked, this method overwrites 544 * {@linkplain #userInfo(String) user-info}, {@linkplain #host(String) host}, 545 * {@linkplain #port(int) port}, {@linkplain #path(String) path}, and 546 * {@link #query(String) query}. 547 * @param ssp the URI scheme-specific-part, may contain URI template parameters 548 * @return this UriComponentsBuilder 549 */ 550 public UriComponentsBuilder schemeSpecificPart(String ssp) { 551 this.ssp = ssp; 552 resetHierarchicalComponents(); 553 return this; 554 } 555 556 @Override 557 public UriComponentsBuilder userInfo(@Nullable String userInfo) { 558 this.userInfo = userInfo; 559 resetSchemeSpecificPart(); 560 return this; 561 } 562 563 @Override 564 public UriComponentsBuilder host(@Nullable String host) { 565 this.host = host; 566 if (host != null) { 567 resetSchemeSpecificPart(); 568 } 569 return this; 570 } 571 572 @Override 573 public UriComponentsBuilder port(int port) { 574 Assert.isTrue(port >= -1, "Port must be >= -1"); 575 this.port = String.valueOf(port); 576 if (port > -1) { 577 resetSchemeSpecificPart(); 578 } 579 return this; 580 } 581 582 @Override 583 public UriComponentsBuilder port(@Nullable String port) { 584 this.port = port; 585 if (port != null) { 586 resetSchemeSpecificPart(); 587 } 588 return this; 589 } 590 591 @Override 592 public UriComponentsBuilder path(String path) { 593 this.pathBuilder.addPath(path); 594 resetSchemeSpecificPart(); 595 return this; 596 } 597 598 @Override 599 public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException { 600 this.pathBuilder.addPathSegments(pathSegments); 601 resetSchemeSpecificPart(); 602 return this; 603 } 604 605 @Override 606 public UriComponentsBuilder replacePath(@Nullable String path) { 607 this.pathBuilder = new CompositePathComponentBuilder(); 608 if (path != null) { 609 this.pathBuilder.addPath(path); 610 } 611 resetSchemeSpecificPart(); 612 return this; 613 } 614 615 @Override 616 public UriComponentsBuilder query(@Nullable String query) { 617 if (query != null) { 618 Matcher matcher = QUERY_PARAM_PATTERN.matcher(query); 619 while (matcher.find()) { 620 String name = matcher.group(1); 621 String eq = matcher.group(2); 622 String value = matcher.group(3); 623 queryParam(name, (value != null ? value : (StringUtils.hasLength(eq) ? "" : null))); 624 } 625 resetSchemeSpecificPart(); 626 } 627 else { 628 this.queryParams.clear(); 629 } 630 return this; 631 } 632 633 @Override 634 public UriComponentsBuilder replaceQuery(@Nullable String query) { 635 this.queryParams.clear(); 636 if (query != null) { 637 query(query); 638 resetSchemeSpecificPart(); 639 } 640 return this; 641 } 642 643 @Override 644 public UriComponentsBuilder queryParam(String name, Object... values) { 645 Assert.notNull(name, "Name must not be null"); 646 if (!ObjectUtils.isEmpty(values)) { 647 for (Object value : values) { 648 String valueAsString = (value != null ? value.toString() : null); 649 this.queryParams.add(name, valueAsString); 650 } 651 } 652 else { 653 this.queryParams.add(name, null); 654 } 655 resetSchemeSpecificPart(); 656 return this; 657 } 658 659 @Override 660 public UriComponentsBuilder queryParam(String name, @Nullable Collection<?> values) { 661 return queryParam(name, (CollectionUtils.isEmpty(values) ? EMPTY_VALUES : values.toArray())); 662 } 663 664 /** 665 * {@inheritDoc} 666 * @since 4.0 667 */ 668 @Override 669 public UriComponentsBuilder queryParams(@Nullable MultiValueMap<String, String> params) { 670 if (params != null) { 671 this.queryParams.addAll(params); 672 resetSchemeSpecificPart(); 673 } 674 return this; 675 } 676 677 @Override 678 public UriComponentsBuilder replaceQueryParam(String name, Object... values) { 679 Assert.notNull(name, "Name must not be null"); 680 this.queryParams.remove(name); 681 if (!ObjectUtils.isEmpty(values)) { 682 queryParam(name, values); 683 } 684 resetSchemeSpecificPart(); 685 return this; 686 } 687 688 @Override 689 public UriComponentsBuilder replaceQueryParam(String name, @Nullable Collection<?> values) { 690 return replaceQueryParam(name, (CollectionUtils.isEmpty(values) ? EMPTY_VALUES : values.toArray())); 691 } 692 693 /** 694 * {@inheritDoc} 695 * @since 4.2 696 */ 697 @Override 698 public UriComponentsBuilder replaceQueryParams(@Nullable MultiValueMap<String, String> params) { 699 this.queryParams.clear(); 700 if (params != null) { 701 this.queryParams.putAll(params); 702 } 703 return this; 704 } 705 706 @Override 707 public UriComponentsBuilder fragment(@Nullable String fragment) { 708 if (fragment != null) { 709 Assert.hasLength(fragment, "Fragment must not be empty"); 710 this.fragment = fragment; 711 } 712 else { 713 this.fragment = null; 714 } 715 return this; 716 } 717 718 /** 719 * Configure URI variables to be expanded at build time. 720 * <p>The provided variables may be a subset of all required ones. At build 721 * time, the available ones are expanded, while unresolved URI placeholders 722 * are left in place and can still be expanded later. 723 * <p>In contrast to {@link UriComponents#expand(Map)} or 724 * {@link #buildAndExpand(Map)}, this method is useful when you need to 725 * supply URI variables without building the {@link UriComponents} instance 726 * just yet, or perhaps pre-expand some shared default values such as host 727 * and port. 728 * @param uriVariables the URI variables to use 729 * @return this UriComponentsBuilder 730 * @since 5.0.8 731 */ 732 public UriComponentsBuilder uriVariables(Map<String, Object> uriVariables) { 733 this.uriVariables.putAll(uriVariables); 734 return this; 735 } 736 737 /** 738 * Adapt this builder's scheme+host+port from the given headers, specifically 739 * "Forwarded" (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>, 740 * or "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if 741 * "Forwarded" is not found. 742 * <p><strong>Note:</strong> this method uses values from forwarded headers, 743 * if present, in order to reflect the client-originated protocol and address. 744 * Consider using the {@code ForwardedHeaderFilter} in order to choose from a 745 * central place whether to extract and use, or to discard such headers. 746 * See the Spring Framework reference for more on this filter. 747 * @param headers the HTTP headers to consider 748 * @return this UriComponentsBuilder 749 * @since 4.2.7 750 */ 751 UriComponentsBuilder adaptFromForwardedHeaders(HttpHeaders headers) { 752 try { 753 String forwardedHeader = headers.getFirst("Forwarded"); 754 if (StringUtils.hasText(forwardedHeader)) { 755 Matcher matcher = FORWARDED_PROTO_PATTERN.matcher(forwardedHeader); 756 if (matcher.find()) { 757 scheme(matcher.group(1).trim()); 758 port(null); 759 } 760 else if (isForwardedSslOn(headers)) { 761 scheme("https"); 762 port(null); 763 } 764 matcher = FORWARDED_HOST_PATTERN.matcher(forwardedHeader); 765 if (matcher.find()) { 766 adaptForwardedHost(matcher.group(1).trim()); 767 } 768 } 769 else { 770 String protocolHeader = headers.getFirst("X-Forwarded-Proto"); 771 if (StringUtils.hasText(protocolHeader)) { 772 scheme(StringUtils.tokenizeToStringArray(protocolHeader, ",")[0]); 773 port(null); 774 } 775 else if (isForwardedSslOn(headers)) { 776 scheme("https"); 777 port(null); 778 } 779 String hostHeader = headers.getFirst("X-Forwarded-Host"); 780 if (StringUtils.hasText(hostHeader)) { 781 adaptForwardedHost(StringUtils.tokenizeToStringArray(hostHeader, ",")[0]); 782 } 783 String portHeader = headers.getFirst("X-Forwarded-Port"); 784 if (StringUtils.hasText(portHeader)) { 785 port(Integer.parseInt(StringUtils.tokenizeToStringArray(portHeader, ",")[0])); 786 } 787 } 788 } 789 catch (NumberFormatException ex) { 790 throw new IllegalArgumentException("Failed to parse a port from \"forwarded\"-type headers. " + 791 "If not behind a trusted proxy, consider using ForwardedHeaderFilter " + 792 "with the removeOnly=true. Request headers: " + headers); 793 } 794 795 if (this.scheme != null && ((this.scheme.equals("http") && "80".equals(this.port)) || 796 (this.scheme.equals("https") && "443".equals(this.port)))) { 797 port(null); 798 } 799 800 return this; 801 } 802 803 private boolean isForwardedSslOn(HttpHeaders headers) { 804 String forwardedSsl = headers.getFirst("X-Forwarded-Ssl"); 805 return StringUtils.hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on"); 806 } 807 808 private void adaptForwardedHost(String hostToUse) { 809 int portSeparatorIdx = hostToUse.lastIndexOf(':'); 810 if (portSeparatorIdx > hostToUse.lastIndexOf(']')) { 811 host(hostToUse.substring(0, portSeparatorIdx)); 812 port(Integer.parseInt(hostToUse.substring(portSeparatorIdx + 1))); 813 } 814 else { 815 host(hostToUse); 816 port(null); 817 } 818 } 819 820 private void resetHierarchicalComponents() { 821 this.userInfo = null; 822 this.host = null; 823 this.port = null; 824 this.pathBuilder = new CompositePathComponentBuilder(); 825 this.queryParams.clear(); 826 } 827 828 private void resetSchemeSpecificPart() { 829 this.ssp = null; 830 } 831 832 833 /** 834 * Public declaration of Object's {@code clone()} method. 835 * Delegates to {@link #cloneBuilder()}. 836 */ 837 @Override 838 public Object clone() { 839 return cloneBuilder(); 840 } 841 842 /** 843 * Clone this {@code UriComponentsBuilder}. 844 * @return the cloned {@code UriComponentsBuilder} object 845 * @since 4.2.7 846 */ 847 public UriComponentsBuilder cloneBuilder() { 848 return new UriComponentsBuilder(this); 849 } 850 851 852 private interface PathComponentBuilder { 853 854 @Nullable 855 PathComponent build(); 856 857 PathComponentBuilder cloneBuilder(); 858 } 859 860 861 private static class CompositePathComponentBuilder implements PathComponentBuilder { 862 863 private final LinkedList<PathComponentBuilder> builders = new LinkedList<>(); 864 865 public void addPathSegments(String... pathSegments) { 866 if (!ObjectUtils.isEmpty(pathSegments)) { 867 PathSegmentComponentBuilder psBuilder = getLastBuilder(PathSegmentComponentBuilder.class); 868 FullPathComponentBuilder fpBuilder = getLastBuilder(FullPathComponentBuilder.class); 869 if (psBuilder == null) { 870 psBuilder = new PathSegmentComponentBuilder(); 871 this.builders.add(psBuilder); 872 if (fpBuilder != null) { 873 fpBuilder.removeTrailingSlash(); 874 } 875 } 876 psBuilder.append(pathSegments); 877 } 878 } 879 880 public void addPath(String path) { 881 if (StringUtils.hasText(path)) { 882 PathSegmentComponentBuilder psBuilder = getLastBuilder(PathSegmentComponentBuilder.class); 883 FullPathComponentBuilder fpBuilder = getLastBuilder(FullPathComponentBuilder.class); 884 if (psBuilder != null) { 885 path = (path.startsWith("/") ? path : "/" + path); 886 } 887 if (fpBuilder == null) { 888 fpBuilder = new FullPathComponentBuilder(); 889 this.builders.add(fpBuilder); 890 } 891 fpBuilder.append(path); 892 } 893 } 894 895 @SuppressWarnings("unchecked") 896 @Nullable 897 private <T> T getLastBuilder(Class<T> builderClass) { 898 if (!this.builders.isEmpty()) { 899 PathComponentBuilder last = this.builders.getLast(); 900 if (builderClass.isInstance(last)) { 901 return (T) last; 902 } 903 } 904 return null; 905 } 906 907 @Override 908 public PathComponent build() { 909 int size = this.builders.size(); 910 List<PathComponent> components = new ArrayList<>(size); 911 for (PathComponentBuilder componentBuilder : this.builders) { 912 PathComponent pathComponent = componentBuilder.build(); 913 if (pathComponent != null) { 914 components.add(pathComponent); 915 } 916 } 917 if (components.isEmpty()) { 918 return HierarchicalUriComponents.NULL_PATH_COMPONENT; 919 } 920 if (components.size() == 1) { 921 return components.get(0); 922 } 923 return new HierarchicalUriComponents.PathComponentComposite(components); 924 } 925 926 @Override 927 public CompositePathComponentBuilder cloneBuilder() { 928 CompositePathComponentBuilder compositeBuilder = new CompositePathComponentBuilder(); 929 for (PathComponentBuilder builder : this.builders) { 930 compositeBuilder.builders.add(builder.cloneBuilder()); 931 } 932 return compositeBuilder; 933 } 934 } 935 936 937 private static class FullPathComponentBuilder implements PathComponentBuilder { 938 939 private final StringBuilder path = new StringBuilder(); 940 941 public void append(String path) { 942 this.path.append(path); 943 } 944 945 @Override 946 public PathComponent build() { 947 if (this.path.length() == 0) { 948 return null; 949 } 950 String path = this.path.toString(); 951 while (true) { 952 int index = path.indexOf("//"); 953 if (index == -1) { 954 break; 955 } 956 path = path.substring(0, index) + path.substring(index + 1); 957 } 958 return new HierarchicalUriComponents.FullPathComponent(path); 959 } 960 961 public void removeTrailingSlash() { 962 int index = this.path.length() - 1; 963 if (this.path.charAt(index) == '/') { 964 this.path.deleteCharAt(index); 965 } 966 } 967 968 @Override 969 public FullPathComponentBuilder cloneBuilder() { 970 FullPathComponentBuilder builder = new FullPathComponentBuilder(); 971 builder.append(this.path.toString()); 972 return builder; 973 } 974 } 975 976 977 private static class PathSegmentComponentBuilder implements PathComponentBuilder { 978 979 private final List<String> pathSegments = new LinkedList<>(); 980 981 public void append(String... pathSegments) { 982 for (String pathSegment : pathSegments) { 983 if (StringUtils.hasText(pathSegment)) { 984 this.pathSegments.add(pathSegment); 985 } 986 } 987 } 988 989 @Override 990 public PathComponent build() { 991 return (this.pathSegments.isEmpty() ? null : 992 new HierarchicalUriComponents.PathSegmentComponent(this.pathSegments)); 993 } 994 995 @Override 996 public PathSegmentComponentBuilder cloneBuilder() { 997 PathSegmentComponentBuilder builder = new PathSegmentComponentBuilder(); 998 builder.pathSegments.addAll(this.pathSegments); 999 return builder; 1000 } 1001 } 1002 1003 1004 private enum EncodingHint { ENCODE_TEMPLATE, FULLY_ENCODED, NONE } 1005 1006}