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         this.host = uri.getHost();
338         this.port = uri.getPort();
339         this.encodedUserInfo = uri.getRawUserInfo();
340         this.userInfo = uri.getUserInfo();
341         if (this.encodedAuthority != null && this.host == null) {
342             try {
343                 final URIAuthority uriAuthority = URIAuthority.parse(this.encodedAuthority);
344                 this.encodedUserInfo = uriAuthority.getUserInfo();
345                 this.userInfo = PercentCodec.decode(uriAuthority.getUserInfo(), charset);
346                 this.host = PercentCodec.decode(uriAuthority.getHostName(), charset);
347                 this.port = uriAuthority.getPort();
348             } catch (final URISyntaxException ignore) {
349             }
350         }
351         this.encodedPath = uri.getRawPath();
352         this.pathSegments = parsePath(uri.getRawPath(), charset);
353         this.pathRootless = uri.getRawPath() == null || !uri.getRawPath().startsWith("/");
354         this.encodedQuery = uri.getRawQuery();
355         this.queryParams = parseQuery(uri.getRawQuery(), charset, false);
356         this.encodedFragment = uri.getRawFragment();
357         this.fragment = uri.getFragment();
358         this.charset = charset;
359     }
360 
361     /**
362      * Sets URI scheme.
363      *
364      * @return this.
365      */
366     public URIBuilder setScheme(final String scheme) {
367         this.scheme = !TextUtils.isBlank(scheme) ? scheme : null;
368         return this;
369     }
370 
371     /**
372      * Sets the URI scheme specific part.
373      *
374      * @param schemeSpecificPart
375      * @return this.
376      * @since 5.1
377      */
378     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart) {
379         this.encodedSchemeSpecificPart = schemeSpecificPart;
380         return this;
381     }
382 
383     /**
384      * Sets the URI scheme specific part and append a variable arguments list of NameValuePair instance(s) to this part.
385      *
386      * @param schemeSpecificPart
387      * @param nvps Optional, can be null. Variable arguments list of NameValuePair query parameters to be reused by the specific scheme part
388      * @return this.
389      * @since 5.1
390      */
391     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final NameValuePair... nvps) {
392         return setSchemeSpecificPart(schemeSpecificPart, nvps != null ? Arrays.asList(nvps) : null);
393     }
394 
395     /**
396      * Sets the URI scheme specific part and append a list of NameValuePair to this part.
397      *
398      * @param schemeSpecificPart
399      * @param nvps Optional, can be null. List of query parameters to be reused by the specific scheme part
400      * @return this.
401      * @since 5.1
402      */
403     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final List <NameValuePair> nvps) {
404         this.encodedSchemeSpecificPart = null;
405         if (!TextUtils.isBlank(schemeSpecificPart)) {
406             final StringBuilder sb = new StringBuilder(schemeSpecificPart);
407             if (nvps != null && !nvps.isEmpty()) {
408                 sb.append("?");
409                 formatQuery(sb, nvps, this.charset, false);
410             }
411             this.encodedSchemeSpecificPart = sb.toString();
412         }
413         return this;
414     }
415 
416     /**
417      * Sets URI user info. The value is expected to be unescaped and may contain non ASCII
418      * characters.
419      *
420      * @return this.
421      */
422     public URIBuilder setUserInfo(final String userInfo) {
423         this.userInfo = !TextUtils.isBlank(userInfo) ? userInfo : null;
424         this.encodedSchemeSpecificPart = null;
425         this.encodedAuthority = null;
426         this.encodedUserInfo = null;
427         return this;
428     }
429 
430     /**
431      * Sets URI user info as a combination of username and password. These values are expected to
432      * be unescaped and may contain non ASCII characters.
433      *
434      * @return this.
435      *
436      * @deprecated The use of clear-text passwords in {@link URI}s has been deprecated and is strongly
437      * discouraged.
438      */
439     @Deprecated
440     public URIBuilder setUserInfo(final String username, final String password) {
441         return setUserInfo(username + ':' + password);
442     }
443 
444     /**
445      * Sets URI host.
446      *
447      * @return this.
448      */
449     public URIBuilder setHost(final InetAddress host) {
450         this.host = host != null ? host.getHostAddress() : null;
451         this.encodedSchemeSpecificPart = null;
452         this.encodedAuthority = null;
453         return this;
454     }
455 
456     /**
457      * Sets URI host.
458      *
459      * @return this.
460      */
461     public URIBuilder setHost(final String host) {
462         this.host = host;
463         this.encodedSchemeSpecificPart = null;
464         this.encodedAuthority = null;
465         return this;
466     }
467 
468     /**
469      * Sets the scheme, host name, and port.
470      *
471      * @param httpHost the scheme, host name, and port.
472      * @return this.
473      */
474     public URIBuilder setHttpHost(final HttpHost httpHost ) {
475         setScheme(httpHost.getSchemeName());
476         setHost(httpHost.getHostName());
477         setPort(httpHost.getPort());
478         return this;
479     }
480 
481     /**
482      * Sets URI port.
483      *
484      * @return this.
485      */
486     public URIBuilder setPort(final int port) {
487         this.port = port < 0 ? -1 : port;
488         this.encodedSchemeSpecificPart = null;
489         this.encodedAuthority = null;
490         return this;
491     }
492 
493     /**
494      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
495      *
496      * @return this.
497      */
498     public URIBuilder setPath(final String path) {
499         setPathSegments(path != null ? splitPath(path) : null);
500         this.pathRootless = path != null && !path.startsWith("/");
501         return this;
502     }
503 
504     /**
505      * Appends path to URI. The value is expected to be unescaped and may contain non ASCII characters.
506      *
507      * @return this.
508      */
509     public URIBuilder appendPath(final String path) {
510         if (path != null) {
511             appendPathSegments(splitPath(path));
512         }
513         return this;
514     }
515 
516     /**
517      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
518      *
519      * @return this.
520      */
521     public URIBuilder setPathSegments(final String... pathSegments) {
522         return setPathSegments(Arrays.asList(pathSegments));
523     }
524 
525     /**
526      * Appends segments URI path. The value is expected to be unescaped and may contain non ASCII characters.
527      *
528      * @return this.
529      */
530     public URIBuilder appendPathSegments(final String... pathSegments) {
531         return appendPathSegments(Arrays.asList(pathSegments));
532     }
533 
534     /**
535      * Sets rootless URI path (the first segment does not start with a /).
536      * The value is expected to be unescaped and may contain non ASCII characters.
537      *
538      * @return this.
539      *
540      * @since 5.1
541      */
542     public URIBuilder setPathSegmentsRootless(final String... pathSegments) {
543         return setPathSegmentsRootless(Arrays.asList(pathSegments));
544     }
545 
546     /**
547      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
548      *
549      * @return this.
550      */
551     public URIBuilder setPathSegments(final List<String> pathSegments) {
552         this.pathSegments = pathSegments != null && !pathSegments.isEmpty() ? new ArrayList<>(pathSegments) : null;
553         this.encodedSchemeSpecificPart = null;
554         this.encodedPath = null;
555         this.pathRootless = false;
556         return this;
557     }
558 
559     /**
560      * Appends segments to URI path. The value is expected to be unescaped and may contain non ASCII characters.
561      *
562      * @return this.
563      */
564     public URIBuilder appendPathSegments(final List<String> pathSegments) {
565         if (pathSegments != null && !pathSegments.isEmpty()) {
566             final List<String> segments = new ArrayList<>(getPathSegments());
567             segments.addAll(pathSegments);
568             setPathSegments(segments);
569         }
570         return this;
571     }
572 
573     /**
574      * Sets rootless URI path (the first segment does not start with a /).
575      * The value is expected to be unescaped and may contain non ASCII characters.
576      *
577      * @return this.
578      *
579      * @since 5.1
580      */
581     public URIBuilder setPathSegmentsRootless(final List<String> pathSegments) {
582         this.pathSegments = pathSegments != null && !pathSegments.isEmpty() ? new ArrayList<>(pathSegments) : null;
583         this.encodedSchemeSpecificPart = null;
584         this.encodedPath = null;
585         this.pathRootless = true;
586         return this;
587     }
588 
589     /**
590      * Removes URI query.
591      *
592      * @return this.
593      */
594     public URIBuilder removeQuery() {
595         this.queryParams = null;
596         this.query = null;
597         this.encodedQuery = null;
598         this.encodedSchemeSpecificPart = null;
599         return this;
600     }
601 
602     /**
603      * Sets URI query parameters. The parameter name / values are expected to be unescaped
604      * and may contain non ASCII characters.
605      * <p>
606      * Please note query parameters and custom query component are mutually exclusive. This method
607      * will remove custom query if present.
608      * </p>
609      *
610      * @return this.
611      */
612     public URIBuilder setParameters(final List <NameValuePair> nvps) {
613         if (this.queryParams == null) {
614             this.queryParams = new ArrayList<>();
615         } else {
616             this.queryParams.clear();
617         }
618         this.queryParams.addAll(nvps);
619         this.encodedQuery = null;
620         this.encodedSchemeSpecificPart = null;
621         this.query = null;
622         return this;
623     }
624 
625     /**
626      * Adds URI query parameters. The parameter name / values are expected to be unescaped
627      * and may contain non ASCII characters.
628      * <p>
629      * Please note query parameters and custom query component are mutually exclusive. This method
630      * will remove custom query if present.
631      * </p>
632      *
633      * @return this.
634      */
635     public URIBuilder addParameters(final List <NameValuePair> nvps) {
636         if (this.queryParams == null) {
637             this.queryParams = new ArrayList<>();
638         }
639         this.queryParams.addAll(nvps);
640         this.encodedQuery = null;
641         this.encodedSchemeSpecificPart = null;
642         this.query = null;
643         return this;
644     }
645 
646     /**
647      * Sets URI query parameters. The parameter name / values are expected to be unescaped
648      * and may contain non ASCII characters.
649      * <p>
650      * Please note query parameters and custom query component are mutually exclusive. This method
651      * will remove custom query if present.
652      * </p>
653      *
654      * @return this.
655      */
656     public URIBuilder setParameters(final NameValuePair... nvps) {
657         if (this.queryParams == null) {
658             this.queryParams = new ArrayList<>();
659         } else {
660             this.queryParams.clear();
661         }
662         Collections.addAll(this.queryParams, nvps);
663         this.encodedQuery = null;
664         this.encodedSchemeSpecificPart = null;
665         this.query = null;
666         return this;
667     }
668 
669     /**
670      * Adds parameter to URI query. The parameter name and value are expected to be unescaped
671      * and may contain non ASCII characters.
672      * <p>
673      * Please note query parameters and custom query component are mutually exclusive. This method
674      * will remove custom query if present.
675      * </p>
676      *
677      * @return this.
678      */
679     public URIBuilder addParameter(final String param, final String value) {
680         if (this.queryParams == null) {
681             this.queryParams = new ArrayList<>();
682         }
683         this.queryParams.add(new BasicNameValuePair(param, value));
684         this.encodedQuery = null;
685         this.encodedSchemeSpecificPart = null;
686         this.query = null;
687         return this;
688     }
689 
690     /**
691      * Sets parameter of URI query overriding existing value if set. The parameter name and value
692      * are expected to be unescaped and may contain non ASCII characters.
693      * <p>
694      * Please note query parameters and custom query component are mutually exclusive. This method
695      * will remove custom query if present.
696      * </p>
697      *
698      * @return this.
699      */
700     public URIBuilder setParameter(final String param, final String value) {
701         if (this.queryParams == null) {
702             this.queryParams = new ArrayList<>();
703         }
704         if (!this.queryParams.isEmpty()) {
705             for (final Iterator<NameValuePair> it = this.queryParams.iterator(); it.hasNext(); ) {
706                 final NameValuePair nvp = it.next();
707                 if (nvp.getName().equals(param)) {
708                     it.remove();
709                 }
710             }
711         }
712         this.queryParams.add(new BasicNameValuePair(param, value));
713         this.encodedQuery = null;
714         this.encodedSchemeSpecificPart = null;
715         this.query = null;
716         return this;
717     }
718 
719     /**
720      * Clears URI query parameters.
721      *
722      * @return this.
723      */
724     public URIBuilder clearParameters() {
725         this.queryParams = null;
726         this.encodedQuery = null;
727         this.encodedSchemeSpecificPart = null;
728         return this;
729     }
730 
731     /**
732      * Sets custom URI query. The value is expected to be unescaped and may contain non ASCII
733      * characters.
734      * <p>
735      * Please note query parameters and custom query component are mutually exclusive. This method
736      * will remove query parameters if present.
737      * </p>
738      *
739      * @return this.
740      */
741     public URIBuilder setCustomQuery(final String query) {
742         this.query = !TextUtils.isBlank(query) ? query : null;
743         this.encodedQuery = null;
744         this.encodedSchemeSpecificPart = null;
745         this.queryParams = null;
746         return this;
747     }
748 
749     /**
750      * Sets URI fragment. The value is expected to be unescaped and may contain non ASCII
751      * characters.
752      *
753      * @return this.
754      */
755     public URIBuilder setFragment(final String fragment) {
756         this.fragment = !TextUtils.isBlank(fragment) ? fragment : null;
757         this.encodedFragment = null;
758         return this;
759     }
760 
761     public boolean isAbsolute() {
762         return this.scheme != null;
763     }
764 
765     public boolean isOpaque() {
766         return this.pathSegments == null && this.encodedPath == null;
767     }
768 
769     public String getScheme() {
770         return this.scheme;
771     }
772 
773     /**
774      * Gets the scheme specific part
775      *
776      * @return String
777      * @since 5.1
778      */
779     public String getSchemeSpecificPart() {
780         return this.encodedSchemeSpecificPart;
781     }
782 
783     public String getUserInfo() {
784         return this.userInfo;
785     }
786 
787     public String getHost() {
788         return this.host;
789     }
790 
791     public int getPort() {
792         return this.port;
793     }
794 
795     public boolean isPathEmpty() {
796         return (this.pathSegments == null || this.pathSegments.isEmpty()) &&
797                 (this.encodedPath == null || this.encodedPath.isEmpty());
798     }
799 
800     public List<String> getPathSegments() {
801         return this.pathSegments != null ? new ArrayList<>(this.pathSegments) : Collections.<String>emptyList();
802     }
803 
804     public String getPath() {
805         if (this.pathSegments == null) {
806             return null;
807         }
808         final StringBuilder result = new StringBuilder();
809         for (final String segment : this.pathSegments) {
810             result.append('/').append(segment);
811         }
812         return result.toString();
813     }
814 
815     public boolean isQueryEmpty() {
816         return (this.queryParams == null || this.queryParams.isEmpty()) && this.encodedQuery == null;
817     }
818 
819     public List<NameValuePair> getQueryParams() {
820         return this.queryParams != null ? new ArrayList<>(this.queryParams) : Collections.<NameValuePair>emptyList();
821     }
822 
823     public String getFragment() {
824         return this.fragment;
825     }
826 
827     /**
828      * Normalizes syntax of URI components if the URI is considered non-opaque
829      * (the path component has a root):
830      * <ul>
831      *  <li>characters of scheme and host components are converted to lower case</li>
832      *  <li>dot segments of the path component are removed if the path has a root</li>
833      *  <li>percent encoding of all components is normalized</li>
834      *
835      * @since 5.1
836      */
837     public URIBuilder normalizeSyntax() {
838         final String scheme = this.scheme;
839         if (scheme != null) {
840             this.scheme = scheme.toLowerCase(Locale.ROOT);
841         }
842 
843         if (this.pathRootless) {
844             return this;
845         }
846 
847         // Force Percent-Encoding normalization
848         this.encodedSchemeSpecificPart = null;
849         this.encodedAuthority = null;
850         this.encodedUserInfo = null;
851         this.encodedPath = null;
852         this.encodedQuery = null;
853         this.encodedFragment = null;
854 
855         final String host = this.host;
856         if (host != null) {
857             this.host = host.toLowerCase(Locale.ROOT);
858         }
859 
860         if (this.pathSegments != null) {
861             final List<String> inputSegments = this.pathSegments;
862             if (!inputSegments.isEmpty()) {
863                 final Stack<String> outputSegments = new Stack<>();
864                 for (final String inputSegment : inputSegments) {
865                     if (!inputSegment.isEmpty() && !".".equals(inputSegment)) {
866                         if ("..".equals(inputSegment)) {
867                             if (!outputSegments.isEmpty()) {
868                                 outputSegments.pop();
869                             }
870                         } else {
871                             outputSegments.push(inputSegment);
872                         }
873                     }
874                 }
875                 if (!inputSegments.isEmpty()) {
876                     final String lastSegment = inputSegments.get(inputSegments.size() - 1);
877                     if (lastSegment.isEmpty()) {
878                         outputSegments.push("");
879                     }
880                 }
881                 this.pathSegments = outputSegments;
882             } else {
883                 this.pathSegments = Collections.singletonList("");
884             }
885         }
886 
887         return this;
888     }
889 
890     @Override
891     public String toString() {
892         return buildString();
893     }
894 
895 }