View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.core5.net;
28  
29  import java.net.InetAddress;
30  import java.net.URI;
31  import java.net.URISyntaxException;
32  import java.net.UnknownHostException;
33  import java.nio.charset.Charset;
34  import java.nio.charset.StandardCharsets;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.BitSet;
38  import java.util.Collections;
39  import java.util.Iterator;
40  import java.util.List;
41  import java.util.Locale;
42  import java.util.Stack;
43  
44  import org.apache.hc.core5.http.HttpHost;
45  import org.apache.hc.core5.http.NameValuePair;
46  import org.apache.hc.core5.http.message.BasicNameValuePair;
47  import org.apache.hc.core5.http.message.ParserCursor;
48  import org.apache.hc.core5.util.TextUtils;
49  import org.apache.hc.core5.util.Tokenizer;
50  
51  /**
52   * Builder for {@link URI} instances.
53   *
54   * @since 5.0
55   */
56  public class URIBuilder {
57  
58      /**
59       * Creates a new builder for the host {@link InetAddress#getLocalHost()}.
60       *
61       * @return a new builder.
62       * @throws UnknownHostException if the local host name could not be resolved into an address.
63       */
64      public static URIBuilder localhost() throws UnknownHostException {
65          return new URIBuilder().setHost(InetAddress.getLocalHost());
66      }
67  
68      /**
69       * Creates a new builder for the host {@link InetAddress#getLoopbackAddress()}.
70       */
71      public static URIBuilder loopbackAddress() {
72          return new URIBuilder().setHost(InetAddress.getLoopbackAddress());
73      }
74  
75      private String scheme;
76      private String encodedSchemeSpecificPart;
77      private String encodedAuthority;
78      private String userInfo;
79      private String encodedUserInfo;
80      private String host;
81      private int port;
82      private String encodedPath;
83      private boolean pathRootless;
84      private List<String> pathSegments;
85      private String encodedQuery;
86      private List<NameValuePair> queryParams;
87      private String query;
88      private Charset charset;
89      private String fragment;
90      private String encodedFragment;
91  
92      /**
93       * Constructs an empty instance.
94       */
95      public URIBuilder() {
96          super();
97          this.port = -1;
98      }
99  
100     /**
101      * Construct an instance from the string which must be a valid URI.
102      *
103      * @param string a valid URI in string form
104      * @throws URISyntaxException if the input is not a valid URI
105      */
106     public URIBuilder(final String string) throws URISyntaxException {
107         this(new URI(string), StandardCharsets.UTF_8);
108     }
109 
110     /**
111      * Construct an instance from the provided URI.
112      * @param uri
113      */
114     public URIBuilder(final URI uri) {
115         this(uri, StandardCharsets.UTF_8);
116     }
117 
118     /**
119      * Construct an instance from the string which must be a valid URI.
120      *
121      * @param string a valid URI in string form
122      * @throws URISyntaxException if the input is not a valid URI
123      */
124     public URIBuilder(final String string, final Charset charset) throws URISyntaxException {
125         this(new URI(string), charset);
126     }
127 
128     /**
129      * Construct an instance from the provided URI.
130      * @param uri
131      */
132     public URIBuilder(final URI uri, final Charset charset) {
133         super();
134         digestURI(uri, charset);
135     }
136 
137     public URIBuilder setCharset(final Charset charset) {
138         this.charset = charset;
139         return this;
140     }
141 
142     public Charset getCharset() {
143         return charset;
144     }
145 
146     private static final char QUERY_PARAM_SEPARATOR = '&';
147     private static final char PARAM_VALUE_SEPARATOR = '=';
148     private static final char PATH_SEPARATOR = '/';
149 
150     private static final BitSet QUERY_PARAM_SEPARATORS = new BitSet(256);
151     private static final BitSet QUERY_VALUE_SEPARATORS = new BitSet(256);
152     private static final BitSet PATH_SEPARATORS = new BitSet(256);
153 
154     static {
155         QUERY_PARAM_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
156         QUERY_PARAM_SEPARATORS.set(PARAM_VALUE_SEPARATOR);
157         QUERY_VALUE_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
158         PATH_SEPARATORS.set(PATH_SEPARATOR);
159     }
160 
161     static List<NameValuePair> parseQuery(final CharSequence s, final Charset charset, final boolean plusAsBlank) {
162         if (s == null) {
163             return null;
164         }
165         final Tokenizer tokenParser = Tokenizer.INSTANCE;
166         final ParserCursore/ParserCursor.html#ParserCursor">ParserCursor cursor = new ParserCursor(0, s.length());
167         final List<NameValuePair> list = new ArrayList<>();
168         while (!cursor.atEnd()) {
169             final String name = tokenParser.parseToken(s, cursor, QUERY_PARAM_SEPARATORS);
170             String value = null;
171             if (!cursor.atEnd()) {
172                 final int delim = s.charAt(cursor.getPos());
173                 cursor.updatePos(cursor.getPos() + 1);
174                 if (delim == PARAM_VALUE_SEPARATOR) {
175                     value = tokenParser.parseToken(s, cursor, QUERY_VALUE_SEPARATORS);
176                     if (!cursor.atEnd()) {
177                         cursor.updatePos(cursor.getPos() + 1);
178                     }
179                 }
180             }
181             if (!name.isEmpty()) {
182                 list.add(new BasicNameValuePair(
183                         PercentCodec.decode(name, charset, plusAsBlank),
184                         PercentCodec.decode(value, charset, plusAsBlank)));
185             }
186         }
187         return list;
188     }
189 
190     static List<String> splitPath(final CharSequence s) {
191         if (s == null) {
192             return null;
193         }
194         final ParserCursore/ParserCursor.html#ParserCursor">ParserCursor cursor = new ParserCursor(0, s.length());
195         // Skip leading separator
196         if (cursor.atEnd()) {
197             return new ArrayList<>(0);
198         }
199         if (PATH_SEPARATORS.get(s.charAt(cursor.getPos()))) {
200             cursor.updatePos(cursor.getPos() + 1);
201         }
202         final List<String> list = new ArrayList<>();
203         final StringBuilder buf = new StringBuilder();
204         for (;;) {
205             if (cursor.atEnd()) {
206                 list.add(buf.toString());
207                 break;
208             }
209             final char current = s.charAt(cursor.getPos());
210             if (PATH_SEPARATORS.get(current)) {
211                 list.add(buf.toString());
212                 buf.setLength(0);
213             } else {
214                 buf.append(current);
215             }
216             cursor.updatePos(cursor.getPos() + 1);
217         }
218         return list;
219     }
220 
221     static List<String> parsePath(final CharSequence s, final Charset charset) {
222         if (s == null) {
223             return null;
224         }
225         final List<String> segments = splitPath(s);
226         final List<String> list = new ArrayList<>(segments.size());
227         for (final String segment: segments) {
228             list.add(PercentCodec.decode(segment, charset));
229         }
230         return list;
231     }
232 
233     static void formatPath(final StringBuilder buf, final Iterable<String> segments, final boolean rootless, final Charset charset) {
234         int i = 0;
235         for (final String segment : segments) {
236             if (i > 0 || !rootless) {
237                 buf.append(PATH_SEPARATOR);
238             }
239             PercentCodec.encode(buf, segment, charset);
240             i++;
241         }
242     }
243 
244     static void formatQuery(final StringBuilder buf, final Iterable<? extends NameValuePair> params, final Charset charset,
245                             final boolean blankAsPlus) {
246         int i = 0;
247         for (final NameValuePair parameter : params) {
248             if (i > 0) {
249                 buf.append(QUERY_PARAM_SEPARATOR);
250             }
251             PercentCodec.encode(buf, parameter.getName(), charset, blankAsPlus);
252             if (parameter.getValue() != null) {
253                 buf.append(PARAM_VALUE_SEPARATOR);
254                 PercentCodec.encode(buf, parameter.getValue(), charset, blankAsPlus);
255             }
256             i++;
257         }
258     }
259 
260     /**
261      * Builds a {@link URI} instance.
262      */
263     public URI build() throws URISyntaxException {
264         return new URI(buildString());
265     }
266 
267     private String buildString() {
268         final StringBuilder sb = new StringBuilder();
269         if (this.scheme != null) {
270             sb.append(this.scheme).append(':');
271         }
272         if (this.encodedSchemeSpecificPart != null) {
273             sb.append(this.encodedSchemeSpecificPart);
274         } else {
275             final boolean authoritySpecified;
276             if (this.encodedAuthority != null) {
277                 sb.append("//").append(this.encodedAuthority);
278                 authoritySpecified = true;
279             } else if (this.host != null) {
280                 sb.append("//");
281                 if (this.encodedUserInfo != null) {
282                     sb.append(this.encodedUserInfo).append("@");
283                 } else if (this.userInfo != null) {
284                     final int idx = this.userInfo.indexOf(':');
285                     if (idx != -1) {
286                         PercentCodec.encode(sb, this.userInfo.substring(0, idx), this.charset);
287                         sb.append(':');
288                         PercentCodec.encode(sb, this.userInfo.substring(idx + 1), this.charset);
289                     } else {
290                         PercentCodec.encode(sb, this.userInfo, this.charset);
291                     }
292                     sb.append("@");
293                 }
294                 if (InetAddressUtils.isIPv6Address(this.host)) {
295                     sb.append("[").append(this.host).append("]");
296                 } else {
297                     sb.append(PercentCodec.encode(this.host, this.charset));
298                 }
299                 if (this.port >= 0) {
300                     sb.append(":").append(this.port);
301                 }
302                 authoritySpecified = true;
303             } else {
304                 authoritySpecified = false;
305             }
306             if (this.encodedPath != null) {
307                 if (authoritySpecified && !TextUtils.isEmpty(this.encodedPath) && !this.encodedPath.startsWith("/")) {
308                     sb.append('/');
309                 }
310                 sb.append(this.encodedPath);
311             } else if (this.pathSegments != null) {
312                 formatPath(sb, this.pathSegments, !authoritySpecified && this.pathRootless, this.charset);
313             }
314             if (this.encodedQuery != null) {
315                 sb.append("?").append(this.encodedQuery);
316             } else if (this.queryParams != null && !this.queryParams.isEmpty()) {
317                 sb.append("?");
318                 formatQuery(sb, this.queryParams, this.charset, false);
319             } else if (this.query != null) {
320                 sb.append("?");
321                 PercentCodec.encode(sb, this.query, this.charset, PercentCodec.URIC, false);
322             }
323         }
324         if (this.encodedFragment != null) {
325             sb.append("#").append(this.encodedFragment);
326         } else if (this.fragment != null) {
327             sb.append("#");
328             PercentCodec.encode(sb, this.fragment, this.charset);
329         }
330         return sb.toString();
331     }
332 
333     private void digestURI(final URI uri, final Charset charset) {
334         this.scheme = uri.getScheme();
335         this.encodedSchemeSpecificPart = uri.getRawSchemeSpecificPart();
336         this.encodedAuthority = uri.getRawAuthority();
337         final String uriHost = uri.getHost();
338         // URI.getHost incorrectly returns bracketed (encoded) IPv6 values. Brackets are an
339         // encoding detail of the URI and not part of the host string.
340         this.host = uriHost != null && InetAddressUtils.isIPv6URLBracketedAddress(uriHost)
341                 ? uriHost.substring(1, uriHost.length() - 1)
342                 : uriHost;
343         this.port = uri.getPort();
344         this.encodedUserInfo = uri.getRawUserInfo();
345         this.userInfo = uri.getUserInfo();
346         if (this.encodedAuthority != null && this.host == null) {
347             try {
348                 final URIAuthority uriAuthority = URIAuthority.parse(this.encodedAuthority);
349                 this.encodedUserInfo = uriAuthority.getUserInfo();
350                 this.userInfo = PercentCodec.decode(uriAuthority.getUserInfo(), charset);
351                 this.host = PercentCodec.decode(uriAuthority.getHostName(), charset);
352                 this.port = uriAuthority.getPort();
353             } catch (final URISyntaxException ignore) {
354             }
355         }
356         this.encodedPath = uri.getRawPath();
357         this.pathSegments = parsePath(uri.getRawPath(), charset);
358         this.pathRootless = uri.getRawPath() == null || !uri.getRawPath().startsWith("/");
359         this.encodedQuery = uri.getRawQuery();
360         this.queryParams = parseQuery(uri.getRawQuery(), charset, false);
361         this.encodedFragment = uri.getRawFragment();
362         this.fragment = uri.getFragment();
363         this.charset = charset;
364     }
365 
366     /**
367      * Sets URI scheme.
368      *
369      * @return this.
370      */
371     public URIBuilder setScheme(final String scheme) {
372         this.scheme = !TextUtils.isBlank(scheme) ? scheme : null;
373         return this;
374     }
375 
376     /**
377      * Sets the URI scheme specific part.
378      *
379      * @param schemeSpecificPart
380      * @return this.
381      * @since 5.1
382      */
383     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart) {
384         this.encodedSchemeSpecificPart = schemeSpecificPart;
385         return this;
386     }
387 
388     /**
389      * Sets the URI scheme specific part and append a variable arguments list of NameValuePair instance(s) to this part.
390      *
391      * @param schemeSpecificPart
392      * @param nvps Optional, can be null. Variable arguments list of NameValuePair query parameters to be reused by the specific scheme part
393      * @return this.
394      * @since 5.1
395      */
396     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final NameValuePair... nvps) {
397         return setSchemeSpecificPart(schemeSpecificPart, nvps != null ? Arrays.asList(nvps) : null);
398     }
399 
400     /**
401      * Sets the URI scheme specific part and append a list of NameValuePair to this part.
402      *
403      * @param schemeSpecificPart
404      * @param nvps Optional, can be null. List of query parameters to be reused by the specific scheme part
405      * @return this.
406      * @since 5.1
407      */
408     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final List <NameValuePair> nvps) {
409         this.encodedSchemeSpecificPart = null;
410         if (!TextUtils.isBlank(schemeSpecificPart)) {
411             final StringBuilder sb = new StringBuilder(schemeSpecificPart);
412             if (nvps != null && !nvps.isEmpty()) {
413                 sb.append("?");
414                 formatQuery(sb, nvps, this.charset, false);
415             }
416             this.encodedSchemeSpecificPart = sb.toString();
417         }
418         return this;
419     }
420 
421     /**
422      * Sets URI user info. The value is expected to be unescaped and may contain non ASCII
423      * characters.
424      *
425      * @return this.
426      */
427     public URIBuilder setUserInfo(final String userInfo) {
428         this.userInfo = !TextUtils.isBlank(userInfo) ? userInfo : null;
429         this.encodedSchemeSpecificPart = null;
430         this.encodedAuthority = null;
431         this.encodedUserInfo = null;
432         return this;
433     }
434 
435     /**
436      * Sets URI user info as a combination of username and password. These values are expected to
437      * be unescaped and may contain non ASCII characters.
438      *
439      * @return this.
440      *
441      * @deprecated The use of clear-text passwords in {@link URI}s has been deprecated and is strongly
442      * discouraged.
443      */
444     @Deprecated
445     public URIBuilder setUserInfo(final String username, final String password) {
446         return setUserInfo(username + ':' + password);
447     }
448 
449     /**
450      * Sets URI host.
451      *
452      * @return this.
453      */
454     public URIBuilder setHost(final InetAddress host) {
455         this.host = host != null ? host.getHostAddress() : null;
456         this.encodedSchemeSpecificPart = null;
457         this.encodedAuthority = null;
458         return this;
459     }
460 
461     /**
462      * Sets URI host. The input value must not already be URI encoded, for example {@code ::1} is valid however
463      * {@code [::1]} is not. It is dangerous to call {@code uriBuilder.setHost(uri.getHost())} due
464      * to {@link URI#getHost()} returning URI encoded values.
465      *
466      * @return this.
467      */
468     public URIBuilder setHost(final String host) {
469         this.host = host;
470         this.encodedSchemeSpecificPart = null;
471         this.encodedAuthority = null;
472         return this;
473     }
474 
475     /**
476      * Sets the scheme, host name, and port.
477      *
478      * @param httpHost the scheme, host name, and port.
479      * @return this.
480      */
481     public URIBuilder setHttpHost(final HttpHost httpHost ) {
482         setScheme(httpHost.getSchemeName());
483         setHost(httpHost.getHostName());
484         setPort(httpHost.getPort());
485         return this;
486     }
487 
488     /**
489      * Sets URI port.
490      *
491      * @return this.
492      */
493     public URIBuilder setPort(final int port) {
494         this.port = port < 0 ? -1 : port;
495         this.encodedSchemeSpecificPart = null;
496         this.encodedAuthority = null;
497         return this;
498     }
499 
500     /**
501      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
502      *
503      * @return this.
504      */
505     public URIBuilder setPath(final String path) {
506         setPathSegments(path != null ? splitPath(path) : null);
507         this.pathRootless = path != null && !path.startsWith("/");
508         return this;
509     }
510 
511     /**
512      * Appends path to URI. The value is expected to be unescaped and may contain non ASCII characters.
513      *
514      * @return this.
515      */
516     public URIBuilder appendPath(final String path) {
517         if (path != null) {
518             appendPathSegments(splitPath(path));
519         }
520         return this;
521     }
522 
523     /**
524      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
525      *
526      * @return this.
527      */
528     public URIBuilder setPathSegments(final String... pathSegments) {
529         return setPathSegments(Arrays.asList(pathSegments));
530     }
531 
532     /**
533      * Appends segments URI path. The value is expected to be unescaped and may contain non ASCII characters.
534      *
535      * @return this.
536      */
537     public URIBuilder appendPathSegments(final String... pathSegments) {
538         return appendPathSegments(Arrays.asList(pathSegments));
539     }
540 
541     /**
542      * Sets rootless URI path (the first segment does not start with a /).
543      * The value is expected to be unescaped and may contain non ASCII characters.
544      *
545      * @return this.
546      *
547      * @since 5.1
548      */
549     public URIBuilder setPathSegmentsRootless(final String... pathSegments) {
550         return setPathSegmentsRootless(Arrays.asList(pathSegments));
551     }
552 
553     /**
554      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
555      *
556      * @return this.
557      */
558     public URIBuilder setPathSegments(final List<String> pathSegments) {
559         this.pathSegments = pathSegments != null && !pathSegments.isEmpty() ? new ArrayList<>(pathSegments) : null;
560         this.encodedSchemeSpecificPart = null;
561         this.encodedPath = null;
562         this.pathRootless = false;
563         return this;
564     }
565 
566     /**
567      * Appends segments to URI path. The value is expected to be unescaped and may contain non ASCII characters.
568      *
569      * @return this.
570      */
571     public URIBuilder appendPathSegments(final List<String> pathSegments) {
572         if (pathSegments != null && !pathSegments.isEmpty()) {
573             if (this.pathSegments == null) {
574                 this.pathSegments = new ArrayList<>();
575             }
576             this.pathSegments.addAll(pathSegments);
577             this.encodedSchemeSpecificPart = null;
578             this.encodedPath = null;
579         }
580         return this;
581     }
582 
583     /**
584      * Sets rootless URI path (the first segment does not start with a /).
585      * The value is expected to be unescaped and may contain non ASCII characters.
586      *
587      * @return this.
588      *
589      * @since 5.1
590      */
591     public URIBuilder setPathSegmentsRootless(final List<String> pathSegments) {
592         this.pathSegments = pathSegments != null && !pathSegments.isEmpty() ? new ArrayList<>(pathSegments) : null;
593         this.encodedSchemeSpecificPart = null;
594         this.encodedPath = null;
595         this.pathRootless = true;
596         return this;
597     }
598 
599     /**
600      * Removes URI query.
601      *
602      * @return this.
603      */
604     public URIBuilder removeQuery() {
605         this.queryParams = null;
606         this.query = null;
607         this.encodedQuery = null;
608         this.encodedSchemeSpecificPart = null;
609         return this;
610     }
611 
612     /**
613      * Sets URI query parameters. The parameter name / values are expected to be unescaped
614      * and may contain non ASCII characters.
615      * <p>
616      * Please note query parameters and custom query component are mutually exclusive. This method
617      * will remove custom query if present.
618      * </p>
619      *
620      * @return this.
621      */
622     public URIBuilder setParameters(final List <NameValuePair> nvps) {
623         if (this.queryParams == null) {
624             this.queryParams = new ArrayList<>();
625         } else {
626             this.queryParams.clear();
627         }
628         this.queryParams.addAll(nvps);
629         this.encodedQuery = null;
630         this.encodedSchemeSpecificPart = null;
631         this.query = null;
632         return this;
633     }
634 
635     /**
636      * Adds URI query parameters. The parameter name / values are expected to be unescaped
637      * and may contain non ASCII characters.
638      * <p>
639      * Please note query parameters and custom query component are mutually exclusive. This method
640      * will remove custom query if present.
641      * </p>
642      *
643      * @return this.
644      */
645     public URIBuilder addParameters(final List <NameValuePair> nvps) {
646         if (this.queryParams == null) {
647             this.queryParams = new ArrayList<>();
648         }
649         this.queryParams.addAll(nvps);
650         this.encodedQuery = null;
651         this.encodedSchemeSpecificPart = null;
652         this.query = null;
653         return this;
654     }
655 
656     /**
657      * Sets URI query parameters. The parameter name / values are expected to be unescaped
658      * and may contain non ASCII characters.
659      * <p>
660      * Please note query parameters and custom query component are mutually exclusive. This method
661      * will remove custom query if present.
662      * </p>
663      *
664      * @return this.
665      */
666     public URIBuilder setParameters(final NameValuePair... nvps) {
667         if (this.queryParams == null) {
668             this.queryParams = new ArrayList<>();
669         } else {
670             this.queryParams.clear();
671         }
672         Collections.addAll(this.queryParams, nvps);
673         this.encodedQuery = null;
674         this.encodedSchemeSpecificPart = null;
675         this.query = null;
676         return this;
677     }
678 
679     /**
680      * Adds parameter to URI query. The parameter name and value are expected to be unescaped
681      * and may contain non ASCII characters.
682      * <p>
683      * Please note query parameters and custom query component are mutually exclusive. This method
684      * will remove custom query if present.
685      * </p>
686      *
687      * @return this.
688      */
689     public URIBuilder addParameter(final String param, final String value) {
690         if (this.queryParams == null) {
691             this.queryParams = new ArrayList<>();
692         }
693         this.queryParams.add(new BasicNameValuePair(param, value));
694         this.encodedQuery = null;
695         this.encodedSchemeSpecificPart = null;
696         this.query = null;
697         return this;
698     }
699 
700     /**
701      * Sets parameter of URI query overriding existing value if set. The parameter name and value
702      * are expected to be unescaped and may contain non ASCII characters.
703      * <p>
704      * Please note query parameters and custom query component are mutually exclusive. This method
705      * will remove custom query if present.
706      * </p>
707      *
708      * @return this.
709      */
710     public URIBuilder setParameter(final String param, final String value) {
711         if (this.queryParams == null) {
712             this.queryParams = new ArrayList<>();
713         }
714         if (!this.queryParams.isEmpty()) {
715             for (final Iterator<NameValuePair> it = this.queryParams.iterator(); it.hasNext(); ) {
716                 final NameValuePair nvp = it.next();
717                 if (nvp.getName().equals(param)) {
718                     it.remove();
719                 }
720             }
721         }
722         this.queryParams.add(new BasicNameValuePair(param, value));
723         this.encodedQuery = null;
724         this.encodedSchemeSpecificPart = null;
725         this.query = null;
726         return this;
727     }
728 
729     /**
730      * Clears URI query parameters.
731      *
732      * @return this.
733      */
734     public URIBuilder clearParameters() {
735         this.queryParams = null;
736         this.encodedQuery = null;
737         this.encodedSchemeSpecificPart = null;
738         return this;
739     }
740 
741     /**
742      * Sets custom URI query. The value is expected to be unescaped and may contain non ASCII
743      * characters.
744      * <p>
745      * Please note query parameters and custom query component are mutually exclusive. This method
746      * will remove query parameters if present.
747      * </p>
748      *
749      * @return this.
750      */
751     public URIBuilder setCustomQuery(final String query) {
752         this.query = !TextUtils.isBlank(query) ? query : null;
753         this.encodedQuery = null;
754         this.encodedSchemeSpecificPart = null;
755         this.queryParams = null;
756         return this;
757     }
758 
759     /**
760      * Sets URI fragment. The value is expected to be unescaped and may contain non ASCII
761      * characters.
762      *
763      * @return this.
764      */
765     public URIBuilder setFragment(final String fragment) {
766         this.fragment = !TextUtils.isBlank(fragment) ? fragment : null;
767         this.encodedFragment = null;
768         return this;
769     }
770 
771     public boolean isAbsolute() {
772         return this.scheme != null;
773     }
774 
775     public boolean isOpaque() {
776         return this.pathSegments == null && this.encodedPath == null;
777     }
778 
779     public String getScheme() {
780         return this.scheme;
781     }
782 
783     /**
784      * Gets the scheme specific part
785      *
786      * @return String
787      * @since 5.1
788      */
789     public String getSchemeSpecificPart() {
790         return this.encodedSchemeSpecificPart;
791     }
792 
793     public String getUserInfo() {
794         return this.userInfo;
795     }
796 
797     /**
798      * Gets the host portion of the {@link URI}. This method returns unencoded IPv6 addresses (without brackets).
799      * This behavior differs from values returned by {@link URI#getHost()}.
800      *
801      * @return The host portion of the URI.
802      */
803     public String getHost() {
804         return this.host;
805     }
806 
807     public int getPort() {
808         return this.port;
809     }
810 
811     public boolean isPathEmpty() {
812         return (this.pathSegments == null || this.pathSegments.isEmpty()) &&
813                 (this.encodedPath == null || this.encodedPath.isEmpty());
814     }
815 
816     public List<String> getPathSegments() {
817         return this.pathSegments != null ? new ArrayList<>(this.pathSegments) : new ArrayList<String>();
818     }
819 
820     public String getPath() {
821         if (this.pathSegments == null) {
822             return null;
823         }
824         final StringBuilder result = new StringBuilder();
825         for (final String segment : this.pathSegments) {
826             result.append('/').append(segment);
827         }
828         return result.toString();
829     }
830 
831     public boolean isQueryEmpty() {
832         return (this.queryParams == null || this.queryParams.isEmpty()) && this.encodedQuery == null;
833     }
834 
835     public List<NameValuePair> getQueryParams() {
836         return this.queryParams != null ? new ArrayList<>(this.queryParams) : new ArrayList<NameValuePair>();
837     }
838 
839     public String getFragment() {
840         return this.fragment;
841     }
842 
843     /**
844      * Normalizes syntax of URI components if the URI is considered non-opaque
845      * (the path component has a root):
846      * <ul>
847      *  <li>characters of scheme and host components are converted to lower case</li>
848      *  <li>dot segments of the path component are removed if the path has a root</li>
849      *  <li>percent encoding of all components is normalized</li>
850      *
851      * @since 5.1
852      */
853     public URIBuilder normalizeSyntax() {
854         final String scheme = this.scheme;
855         if (scheme != null) {
856             this.scheme = scheme.toLowerCase(Locale.ROOT);
857         }
858 
859         if (this.pathRootless) {
860             return this;
861         }
862 
863         // Force Percent-Encoding normalization
864         this.encodedSchemeSpecificPart = null;
865         this.encodedAuthority = null;
866         this.encodedUserInfo = null;
867         this.encodedPath = null;
868         this.encodedQuery = null;
869         this.encodedFragment = null;
870 
871         final String host = this.host;
872         if (host != null) {
873             this.host = host.toLowerCase(Locale.ROOT);
874         }
875 
876         if (this.pathSegments != null) {
877             final List<String> inputSegments = this.pathSegments;
878             if (!inputSegments.isEmpty()) {
879                 final Stack<String> outputSegments = new Stack<>();
880                 for (final String inputSegment : inputSegments) {
881                     if (!inputSegment.isEmpty() && !".".equals(inputSegment)) {
882                         if ("..".equals(inputSegment)) {
883                             if (!outputSegments.isEmpty()) {
884                                 outputSegments.pop();
885                             }
886                         } else {
887                             outputSegments.push(inputSegment);
888                         }
889                     }
890                 }
891                 if (!inputSegments.isEmpty()) {
892                     final String lastSegment = inputSegments.get(inputSegments.size() - 1);
893                     if (lastSegment.isEmpty()) {
894                         outputSegments.push("");
895                     }
896                 }
897                 this.pathSegments = outputSegments;
898             } else {
899                 this.pathSegments = Collections.singletonList("");
900             }
901         }
902 
903         return this;
904     }
905 
906     @Override
907     public String toString() {
908         return buildString();
909     }
910 
911 }