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 = &quot;/hotels/42?filter={value}&quot;;
215         * UriComponentsBuilder.fromUriString(uriString).buildAndExpand(&quot;hot&amp;cold&quot;);
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 = &quot;https://example.com/hotels/42?filter={value}&quot;;
275         * UriComponentsBuilder.fromHttpUrl(urlString).buildAndExpand(&quot;hot&amp;cold&quot;);
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}