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