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.LinkedList;
40  import java.util.List;
41  
42  import org.apache.hc.core5.http.HttpHost;
43  import org.apache.hc.core5.http.NameValuePair;
44  import org.apache.hc.core5.http.message.BasicNameValuePair;
45  import org.apache.hc.core5.http.message.ParserCursor;
46  import org.apache.hc.core5.util.Args;
47  import org.apache.hc.core5.util.TextUtils;
48  import org.apache.hc.core5.util.Tokenizer;
49  
50  /**
51   * Builder for {@link URI} instances.
52   *
53   * @since 5.0
54   */
55  public class URIBuilder {
56  
57      /**
58       * Creates a new builder for the host {@link InetAddress#getLocalHost()}.
59       *
60       * @return a new builder.
61       * @throws UnknownHostException if the local host name could not be resolved into an address.
62       */
63      public static URIBuilder localhost() throws UnknownHostException {
64          return new URIBuilder().setHost(InetAddress.getLocalHost());
65      }
66  
67      /**
68       * Creates a new builder for the host {@link InetAddress#getLoopbackAddress()}.
69       */
70      public static URIBuilder loopbackAddress() {
71          return new URIBuilder().setHost(InetAddress.getLoopbackAddress());
72      }
73  
74      private String scheme;
75      private String encodedSchemeSpecificPart;
76      private String encodedAuthority;
77      private String userInfo;
78      private String encodedUserInfo;
79      private String host;
80      private int port;
81      private String encodedPath;
82      private boolean pathRootless;
83      private List<String> pathSegments;
84      private String encodedQuery;
85      private List<NameValuePair> queryParams;
86      private String query;
87      private Charset charset;
88      private String fragment;
89      private String encodedFragment;
90  
91      /**
92       * Constructs an empty instance.
93       */
94      public URIBuilder() {
95          super();
96          this.port = -1;
97      }
98  
99      /**
100      * Constructs an instance from the string which must be a valid URI.
101      *
102      * @param uriString a valid URI in string form.
103      * @throws URISyntaxException if the input is not a valid URI.
104      */
105     public URIBuilder(final String uriString) throws URISyntaxException {
106         this(new URI(uriString), StandardCharsets.UTF_8);
107     }
108 
109     /**
110      * Constructs an instance from the provided URI.
111      * @param uri a URI.
112      */
113     public URIBuilder(final URI uri) {
114         this(uri, StandardCharsets.UTF_8);
115     }
116 
117     /**
118      * Constructs an instance from the string which must be a valid URI.
119      *
120      * @param uriString a valid URI in string form.
121      * @throws URISyntaxException if the input is not a valid URI
122      */
123     public URIBuilder(final String uriString, final Charset charset) throws URISyntaxException {
124         this(new URI(uriString), charset);
125     }
126 
127     /**
128      * Constructs an instance from the provided URI.
129      *
130      * @param uri a URI.
131      */
132     public URIBuilder(final URI uri, final Charset charset) {
133         super();
134         digestURI(uri, charset);
135     }
136 
137     /**
138      * Sets the authority.
139      *
140      * @param authority the authority.
141      * @return this.
142      * @since 5.2
143      */
144     public URIBuilder setAuthority(final NamedEndpoint authority) {
145         setUserInfo(null);
146         setHost(authority.getHostName());
147         setPort(authority.getPort());
148         return this;
149     }
150 
151     /**
152      * Sets the authority.
153      *
154      * @param authority the authority.
155      * @return this.
156      * @since 5.2
157      */
158     public URIBuilder setAuthority(final URIAuthority authority) {
159         setUserInfo(authority.getUserInfo());
160         setHost(authority.getHostName());
161         setPort(authority.getPort());
162         return this;
163     }
164 
165     /**
166      * Sets the Charset.
167      *
168      * @param charset the Charset.
169      * @return this.
170      */
171     public URIBuilder setCharset(final Charset charset) {
172         this.charset = charset;
173         return this;
174     }
175 
176     /**
177      * Gets the authority.
178      *
179      * @return the authority.
180      * @since 5.2
181      */
182     public URIAuthority getAuthority() {
183         return new URIAuthority(getUserInfo(), getHost(), getPort());
184     }
185 
186     /**
187      * Gets the Charset.
188      *
189      * @return the Charset.
190      */
191     public Charset getCharset() {
192         return charset;
193     }
194 
195     private static final char QUERY_PARAM_SEPARATOR = '&';
196     private static final char PARAM_VALUE_SEPARATOR = '=';
197     private static final char PATH_SEPARATOR = '/';
198 
199     private static final BitSet QUERY_PARAM_SEPARATORS = new BitSet(256);
200     private static final BitSet QUERY_VALUE_SEPARATORS = new BitSet(256);
201     private static final BitSet PATH_SEPARATORS = new BitSet(256);
202 
203     static {
204         QUERY_PARAM_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
205         QUERY_PARAM_SEPARATORS.set(PARAM_VALUE_SEPARATOR);
206         QUERY_VALUE_SEPARATORS.set(QUERY_PARAM_SEPARATOR);
207         PATH_SEPARATORS.set(PATH_SEPARATOR);
208     }
209 
210     static List<NameValuePair> parseQuery(final CharSequence s, final Charset charset, final boolean plusAsBlank) {
211         if (s == null) {
212             return null;
213         }
214         final Tokenizer tokenParser = Tokenizer.INSTANCE;
215         final ParserCursor cursor = new ParserCursor(0, s.length());
216         final List<NameValuePair> list = new ArrayList<>();
217         while (!cursor.atEnd()) {
218             final String name = tokenParser.parseToken(s, cursor, QUERY_PARAM_SEPARATORS);
219             String value = null;
220             if (!cursor.atEnd()) {
221                 final int delim = s.charAt(cursor.getPos());
222                 cursor.updatePos(cursor.getPos() + 1);
223                 if (delim == PARAM_VALUE_SEPARATOR) {
224                     value = tokenParser.parseToken(s, cursor, QUERY_VALUE_SEPARATORS);
225                     if (!cursor.atEnd()) {
226                         cursor.updatePos(cursor.getPos() + 1);
227                     }
228                 }
229             }
230             if (!name.isEmpty()) {
231                 list.add(new BasicNameValuePair(
232                         PercentCodec.decode(name, charset, plusAsBlank),
233                         PercentCodec.decode(value, charset, plusAsBlank)));
234             }
235         }
236         return list;
237     }
238 
239     static List<String> splitPath(final CharSequence s) {
240         if (s == null) {
241             return null;
242         }
243         final ParserCursor cursor = new ParserCursor(0, s.length());
244         // Skip leading separator
245         if (cursor.atEnd()) {
246             return new ArrayList<>(0);
247         }
248         if (PATH_SEPARATORS.get(s.charAt(cursor.getPos()))) {
249             cursor.updatePos(cursor.getPos() + 1);
250         }
251         final List<String> list = new ArrayList<>();
252         final StringBuilder buf = new StringBuilder();
253         for (;;) {
254             if (cursor.atEnd()) {
255                 list.add(buf.toString());
256                 break;
257             }
258             final char current = s.charAt(cursor.getPos());
259             if (PATH_SEPARATORS.get(current)) {
260                 list.add(buf.toString());
261                 buf.setLength(0);
262             } else {
263                 buf.append(current);
264             }
265             cursor.updatePos(cursor.getPos() + 1);
266         }
267         return list;
268     }
269 
270     static List<String> parsePath(final CharSequence s, final Charset charset) {
271         if (s == null) {
272             return null;
273         }
274         final List<String> segments = splitPath(s);
275         final List<String> list = new ArrayList<>(segments.size());
276         for (final String segment: segments) {
277             list.add(PercentCodec.decode(segment, charset));
278         }
279         return list;
280     }
281 
282     static void formatPath(final StringBuilder buf, final Iterable<String> segments, final boolean rootless, final Charset charset) {
283         int i = 0;
284         for (final String segment : segments) {
285             if (i > 0 || !rootless) {
286                 buf.append(PATH_SEPARATOR);
287             }
288             PercentCodec.encode(buf, segment, charset);
289             i++;
290         }
291     }
292 
293     static void formatQuery(final StringBuilder buf, final Iterable<? extends NameValuePair> params, final Charset charset,
294                             final boolean blankAsPlus) {
295         int i = 0;
296         for (final NameValuePair parameter : params) {
297             if (i > 0) {
298                 buf.append(QUERY_PARAM_SEPARATOR);
299             }
300             PercentCodec.encode(buf, parameter.getName(), charset, blankAsPlus);
301             if (parameter.getValue() != null) {
302                 buf.append(PARAM_VALUE_SEPARATOR);
303                 PercentCodec.encode(buf, parameter.getValue(), charset, blankAsPlus);
304             }
305             i++;
306         }
307     }
308 
309     /**
310      * Builds a {@link URI} instance.
311      */
312     public URI build() throws URISyntaxException {
313         return new URI(buildString());
314     }
315 
316     private String buildString() {
317         final StringBuilder sb = new StringBuilder();
318         if (this.scheme != null) {
319             sb.append(this.scheme).append(':');
320         }
321         if (this.encodedSchemeSpecificPart != null) {
322             sb.append(this.encodedSchemeSpecificPart);
323         } else {
324             final boolean authoritySpecified;
325             if (this.encodedAuthority != null) {
326                 sb.append("//").append(this.encodedAuthority);
327                 authoritySpecified = true;
328             } else if (this.host != null) {
329                 sb.append("//");
330                 if (this.encodedUserInfo != null) {
331                     sb.append(this.encodedUserInfo).append("@");
332                 } else if (this.userInfo != null) {
333                     final int idx = this.userInfo.indexOf(':');
334                     if (idx != -1) {
335                         PercentCodec.encode(sb, this.userInfo.substring(0, idx), this.charset);
336                         sb.append(':');
337                         PercentCodec.encode(sb, this.userInfo.substring(idx + 1), this.charset);
338                     } else {
339                         PercentCodec.encode(sb, this.userInfo, this.charset);
340                     }
341                     sb.append("@");
342                 }
343                 if (InetAddressUtils.isIPv6Address(this.host)) {
344                     sb.append("[").append(this.host).append("]");
345                 } else {
346                     sb.append(PercentCodec.encode(this.host, this.charset));
347                 }
348                 if (this.port >= 0) {
349                     sb.append(":").append(this.port);
350                 }
351                 authoritySpecified = true;
352             } else {
353                 authoritySpecified = false;
354             }
355             if (this.encodedPath != null) {
356                 if (authoritySpecified && !TextUtils.isEmpty(this.encodedPath) && !this.encodedPath.startsWith("/")) {
357                     sb.append('/');
358                 }
359                 sb.append(this.encodedPath);
360             } else if (this.pathSegments != null) {
361                 formatPath(sb, this.pathSegments, !authoritySpecified && this.pathRootless, this.charset);
362             }
363             if (this.encodedQuery != null) {
364                 sb.append("?").append(this.encodedQuery);
365             } else if (this.queryParams != null && !this.queryParams.isEmpty()) {
366                 sb.append("?");
367                 formatQuery(sb, this.queryParams, this.charset, false);
368             } else if (this.query != null) {
369                 sb.append("?");
370                 PercentCodec.encode(sb, this.query, this.charset, PercentCodec.URIC, false);
371             }
372         }
373         if (this.encodedFragment != null) {
374             sb.append("#").append(this.encodedFragment);
375         } else if (this.fragment != null) {
376             sb.append("#");
377             PercentCodec.encode(sb, this.fragment, this.charset);
378         }
379         return sb.toString();
380     }
381 
382     private void digestURI(final URI uri, final Charset charset) {
383         this.scheme = uri.getScheme();
384         this.encodedSchemeSpecificPart = uri.getRawSchemeSpecificPart();
385         this.encodedAuthority = uri.getRawAuthority();
386         final String uriHost = uri.getHost();
387         // URI.getHost incorrectly returns bracketed (encoded) IPv6 values. Brackets are an
388         // encoding detail of the URI and not part of the host string.
389         this.host = uriHost != null && InetAddressUtils.isIPv6URLBracketedAddress(uriHost)
390                 ? uriHost.substring(1, uriHost.length() - 1)
391                 : uriHost;
392         this.port = uri.getPort();
393         this.encodedUserInfo = uri.getRawUserInfo();
394         this.userInfo = uri.getUserInfo();
395         if (this.encodedAuthority != null && this.host == null) {
396             try {
397                 final URIAuthority uriAuthority = URIAuthority.parse(this.encodedAuthority);
398                 this.encodedUserInfo = uriAuthority.getUserInfo();
399                 this.userInfo = PercentCodec.decode(uriAuthority.getUserInfo(), charset);
400                 this.host = PercentCodec.decode(uriAuthority.getHostName(), charset);
401                 this.port = uriAuthority.getPort();
402             } catch (final URISyntaxException ignore) {
403                 // ignore
404             }
405         }
406         this.encodedPath = uri.getRawPath();
407         this.pathSegments = parsePath(uri.getRawPath(), charset);
408         this.pathRootless = uri.getRawPath() == null || !uri.getRawPath().startsWith("/");
409         this.encodedQuery = uri.getRawQuery();
410         this.queryParams = parseQuery(uri.getRawQuery(), charset, false);
411         this.encodedFragment = uri.getRawFragment();
412         this.fragment = uri.getFragment();
413         this.charset = charset;
414     }
415 
416     /**
417      * Sets URI scheme.
418      *
419      * @return this.
420      */
421     public URIBuilder setScheme(final String scheme) {
422         this.scheme = !TextUtils.isBlank(scheme) ? scheme : null;
423         return this;
424     }
425 
426     /**
427      * Sets the URI scheme specific part.
428      *
429      * @param schemeSpecificPart
430      * @return this.
431      * @since 5.1
432      */
433     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart) {
434         this.encodedSchemeSpecificPart = schemeSpecificPart;
435         return this;
436     }
437 
438     /**
439      * Sets the URI scheme specific part and append a variable arguments list of NameValuePair instance(s) to this part.
440      *
441      * @param schemeSpecificPart
442      * @param nvps Optional, can be null. Variable arguments list of NameValuePair query parameters to be reused by the specific scheme part
443      * @return this.
444      * @since 5.1
445      */
446     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final NameValuePair... nvps) {
447         return setSchemeSpecificPart(schemeSpecificPart, nvps != null ? Arrays.asList(nvps) : null);
448     }
449 
450     /**
451      * Sets the URI scheme specific part and append a list of NameValuePair to this part.
452      *
453      * @param schemeSpecificPart
454      * @param nvps Optional, can be null. List of query parameters to be reused by the specific scheme part
455      * @return this.
456      * @since 5.1
457      */
458     public URIBuilder setSchemeSpecificPart(final String schemeSpecificPart, final List <NameValuePair> nvps) {
459         this.encodedSchemeSpecificPart = null;
460         if (!TextUtils.isBlank(schemeSpecificPart)) {
461             final StringBuilder sb = new StringBuilder(schemeSpecificPart);
462             if (nvps != null && !nvps.isEmpty()) {
463                 sb.append("?");
464                 formatQuery(sb, nvps, this.charset, false);
465             }
466             this.encodedSchemeSpecificPart = sb.toString();
467         }
468         return this;
469     }
470 
471     /**
472      * Sets URI user info. The value is expected to be unescaped and may contain non ASCII
473      * characters.
474      *
475      * @return this.
476      */
477     public URIBuilder setUserInfo(final String userInfo) {
478         this.userInfo = !TextUtils.isBlank(userInfo) ? userInfo : null;
479         this.encodedSchemeSpecificPart = null;
480         this.encodedAuthority = null;
481         this.encodedUserInfo = null;
482         return this;
483     }
484 
485     /**
486      * Sets URI user info as a combination of username and password. These values are expected to
487      * be unescaped and may contain non ASCII characters.
488      *
489      * @return this.
490      *
491      * @deprecated The use of clear-text passwords in {@link URI}s has been deprecated and is strongly
492      * discouraged.
493      */
494     @Deprecated
495     public URIBuilder setUserInfo(final String username, final String password) {
496         return setUserInfo(username + ':' + password);
497     }
498 
499     /**
500      * Sets URI host.
501      *
502      * @return this.
503      */
504     public URIBuilder setHost(final InetAddress host) {
505         this.host = host != null ? host.getHostAddress() : null;
506         this.encodedSchemeSpecificPart = null;
507         this.encodedAuthority = null;
508         return this;
509     }
510 
511     /**
512      * Sets URI host. The input value must not already be URI encoded, for example {@code ::1} is valid however
513      * {@code [::1]} is not. It is dangerous to call {@code uriBuilder.setHost(uri.getHost())} due
514      * to {@link URI#getHost()} returning URI encoded values.
515      *
516      * @return this.
517      */
518     public URIBuilder setHost(final String host) {
519         this.host = host;
520         this.encodedSchemeSpecificPart = null;
521         this.encodedAuthority = null;
522         return this;
523     }
524 
525     /**
526      * Sets the scheme, host name, and port.
527      *
528      * @param httpHost the scheme, host name, and port.
529      * @return this.
530      */
531     public URIBuilder setHttpHost(final HttpHost httpHost) {
532         setScheme(httpHost.getSchemeName());
533         setHost(httpHost.getHostName());
534         setPort(httpHost.getPort());
535         return this;
536     }
537 
538     /**
539      * Sets URI port.
540      *
541      * @return this.
542      */
543     public URIBuilder setPort(final int port) {
544         this.port = port < 0 ? -1 : port;
545         this.encodedSchemeSpecificPart = null;
546         this.encodedAuthority = null;
547         return this;
548     }
549 
550     /**
551      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
552      *
553      * @return this.
554      */
555     public URIBuilder setPath(final String path) {
556         setPathSegments(path != null ? splitPath(path) : null);
557         this.pathRootless = path != null && !path.startsWith("/");
558         return this;
559     }
560 
561     /**
562      * Appends path to URI. The value is expected to be unescaped and may contain non ASCII characters.
563      *
564      * @return this.
565      */
566     public URIBuilder appendPath(final String path) {
567         if (path != null) {
568             appendPathSegments(splitPath(path));
569         }
570         return this;
571     }
572 
573     /**
574      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
575      *
576      * @return this.
577      */
578     public URIBuilder setPathSegments(final String... pathSegments) {
579         return setPathSegments(Arrays.asList(pathSegments));
580     }
581 
582     /**
583      * Appends segments URI path. The value is expected to be unescaped and may contain non ASCII characters.
584      *
585      * @return this.
586      */
587     public URIBuilder appendPathSegments(final String... pathSegments) {
588         return appendPathSegments(Arrays.asList(pathSegments));
589     }
590 
591     /**
592      * Sets rootless URI path (the first segment does not start with a /).
593      * The value is expected to be unescaped and may contain non ASCII characters.
594      *
595      * @return this.
596      *
597      * @since 5.1
598      */
599     public URIBuilder setPathSegmentsRootless(final String... pathSegments) {
600         return setPathSegmentsRootless(Arrays.asList(pathSegments));
601     }
602 
603     /**
604      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
605      *
606      * @return this.
607      */
608     public URIBuilder setPathSegments(final List<String> pathSegments) {
609         this.pathSegments = pathSegments != null && !pathSegments.isEmpty() ? new ArrayList<>(pathSegments) : null;
610         this.encodedSchemeSpecificPart = null;
611         this.encodedPath = null;
612         this.pathRootless = false;
613         return this;
614     }
615 
616     /**
617      * Appends segments to URI path. The value is expected to be unescaped and may contain non ASCII characters.
618      *
619      * @return this.
620      */
621     public URIBuilder appendPathSegments(final List<String> pathSegments) {
622         if (pathSegments != null && !pathSegments.isEmpty()) {
623             if (this.pathSegments == null) {
624                 this.pathSegments = new ArrayList<>();
625             }
626             this.pathSegments.addAll(pathSegments);
627             this.encodedSchemeSpecificPart = null;
628             this.encodedPath = null;
629         }
630         return this;
631     }
632 
633     /**
634      * Sets rootless URI path (the first segment does not start with a /).
635      * The value is expected to be unescaped and may contain non ASCII characters.
636      *
637      * @return this.
638      *
639      * @since 5.1
640      */
641     public URIBuilder setPathSegmentsRootless(final List<String> pathSegments) {
642         this.pathSegments = pathSegments != null && !pathSegments.isEmpty() ? new ArrayList<>(pathSegments) : null;
643         this.encodedSchemeSpecificPart = null;
644         this.encodedPath = null;
645         this.pathRootless = true;
646         return this;
647     }
648 
649     /**
650      * Removes URI query.
651      *
652      * @return this.
653      */
654     public URIBuilder removeQuery() {
655         this.queryParams = null;
656         this.query = null;
657         this.encodedQuery = null;
658         this.encodedSchemeSpecificPart = null;
659         return this;
660     }
661 
662     /**
663      * Sets URI query parameters. The parameter name / values are expected to be unescaped
664      * and may contain non ASCII characters.
665      * <p>
666      * Please note query parameters and custom query component are mutually exclusive. This method
667      * will remove custom query if present.
668      * </p>
669      *
670      * @return this.
671      */
672     public URIBuilder setParameters(final List <NameValuePair> nameValuePairs) {
673         if (this.queryParams == null) {
674             this.queryParams = new ArrayList<>();
675         } else {
676             this.queryParams.clear();
677         }
678         if (nameValuePairs != null) {
679             this.queryParams.addAll(nameValuePairs);
680         }
681         this.encodedQuery = null;
682         this.encodedSchemeSpecificPart = null;
683         this.query = null;
684         return this;
685     }
686 
687     /**
688      * Adds URI query parameters. The parameter name / values are expected to be unescaped
689      * and may contain non ASCII characters.
690      * <p>
691      * Please note query parameters and custom query component are mutually exclusive. This method
692      * will remove custom query if present.
693      * </p>
694      *
695      * @return this.
696      */
697     public URIBuilder addParameters(final List<NameValuePair> nameValuePairs) {
698         if (this.queryParams == null) {
699             this.queryParams = new ArrayList<>();
700         }
701         if (nameValuePairs != null) {
702             this.queryParams.addAll(nameValuePairs);
703         }
704         this.encodedQuery = null;
705         this.encodedSchemeSpecificPart = null;
706         this.query = null;
707         return this;
708     }
709 
710     /**
711      * Sets URI query parameters. The parameter name / values are expected to be unescaped
712      * and may contain non ASCII characters.
713      * <p>
714      * Please note query parameters and custom query component are mutually exclusive. This method
715      * will remove custom query if present.
716      * </p>
717      *
718      * @return this.
719      */
720     public URIBuilder setParameters(final NameValuePair... nameValuePairs) {
721         if (this.queryParams == null) {
722             this.queryParams = new ArrayList<>();
723         } else {
724             this.queryParams.clear();
725         }
726         if (nameValuePairs != null) {
727             Collections.addAll(this.queryParams, nameValuePairs);
728         }
729         this.encodedQuery = null;
730         this.encodedSchemeSpecificPart = null;
731         this.query = null;
732         return this;
733     }
734 
735     /**
736      * Adds parameter to URI query. The parameter name and value are expected to be unescaped
737      * and may contain non ASCII characters.
738      * <p>
739      * Please note query parameters and custom query component are mutually exclusive. This method
740      * will remove custom query if present.
741      * </p>
742      *
743      * @return this.
744      */
745     public URIBuilder addParameter(final String param, final String value) {
746         return addParameter(new BasicNameValuePair(param, value));
747     }
748 
749     /**
750      * Adds parameter to URI query. The parameter name and value are expected to be unescaped
751      * and may contain non ASCII characters.
752      * <p>
753      * Please note query parameters and custom query component are mutually exclusive. This method
754      * will remove custom query if present.
755      * </p>
756      *
757      * @return this.
758      * @since 5.2
759      */
760     public URIBuilder addParameter(final NameValuePair nameValuePair) {
761         if (this.queryParams == null) {
762             this.queryParams = new ArrayList<>();
763         }
764         if (nameValuePair != null) {
765             this.queryParams.add(nameValuePair);
766         }
767         this.encodedQuery = null;
768         this.encodedSchemeSpecificPart = null;
769         this.query = null;
770         return this;
771     }
772 
773     /**
774      * Removes parameter of URI query if set. The parameter name is expected to be unescaped and may
775      * contain non ASCII characters.
776      * <p>
777      * Please note query parameters and custom query component are mutually exclusive. This method
778      * will remove custom query if present, even when no parameter was actually removed.
779      * </p>
780      *
781      * @return this.
782      * @since 5.2
783      */
784     public URIBuilder removeParameter(final String param) {
785         Args.notNull(param, "param");
786         if (this.queryParams != null && !this.queryParams.isEmpty()) {
787             this.queryParams.removeIf(nvp -> nvp.getName().equals(param));
788         }
789         this.encodedQuery = null;
790         this.encodedSchemeSpecificPart = null;
791         this.query = null;
792         return this;
793     }
794 
795     /**
796      * Sets parameter of URI query overriding existing value if set. The parameter name and value
797      * are expected to be unescaped and may contain non ASCII characters.
798      * <p>
799      * Please note query parameters and custom query component are mutually exclusive. This method
800      * will remove custom query if present.
801      * </p>
802      *
803      * @return this.
804      */
805     public URIBuilder setParameter(final String param, final String value) {
806         if (this.queryParams == null) {
807             this.queryParams = new ArrayList<>();
808         }
809         if (!this.queryParams.isEmpty()) {
810             this.queryParams.removeIf(nvp -> nvp.getName().equals(param));
811         }
812         this.queryParams.add(new BasicNameValuePair(param, value));
813         this.encodedQuery = null;
814         this.encodedSchemeSpecificPart = null;
815         this.query = null;
816         return this;
817     }
818 
819     /**
820      * Clears URI query parameters.
821      *
822      * @return this.
823      */
824     public URIBuilder clearParameters() {
825         this.queryParams = null;
826         this.encodedQuery = null;
827         this.encodedSchemeSpecificPart = null;
828         return this;
829     }
830 
831     /**
832      * Sets custom URI query. The value is expected to be unescaped and may contain non ASCII
833      * characters.
834      * <p>
835      * Please note query parameters and custom query component are mutually exclusive. This method
836      * will remove query parameters if present.
837      * </p>
838      *
839      * @return this.
840      */
841     public URIBuilder setCustomQuery(final String query) {
842         this.query = !TextUtils.isBlank(query) ? query : null;
843         this.encodedQuery = null;
844         this.encodedSchemeSpecificPart = null;
845         this.queryParams = null;
846         return this;
847     }
848 
849     /**
850      * Sets URI fragment. The value is expected to be unescaped and may contain non ASCII
851      * characters.
852      *
853      * @return this.
854      */
855     public URIBuilder setFragment(final String fragment) {
856         this.fragment = !TextUtils.isBlank(fragment) ? fragment : null;
857         this.encodedFragment = null;
858         return this;
859     }
860 
861     /**
862      * Tests whether the URI is absolute.
863      *
864      * @return whether the URI is absolute.
865      */
866     public boolean isAbsolute() {
867         return this.scheme != null;
868     }
869 
870     /**
871      * Tests whether the URI is opaque.
872      *
873      * @return whether the URI is opaque.
874      */
875     public boolean isOpaque() {
876         return this.pathSegments == null && this.encodedPath == null;
877     }
878 
879     /**
880      * Gets the scheme.
881      *
882      * @return the scheme.
883      */
884     public String getScheme() {
885         return this.scheme;
886     }
887 
888     /**
889      * Gets the scheme specific part.
890      *
891      * @return String
892      * @since 5.1
893      */
894     public String getSchemeSpecificPart() {
895         return this.encodedSchemeSpecificPart;
896     }
897 
898     /**
899      * Gets the user info.
900      *
901      * @return  the user info.
902      */
903     public String getUserInfo() {
904         return this.userInfo;
905     }
906 
907     /**
908      * Gets the host portion of the {@link URI}. This method returns unencoded IPv6 addresses (without brackets).
909      * This behavior differs from values returned by {@link URI#getHost()}.
910      *
911      * @return The host portion of the URI.
912      */
913     public String getHost() {
914         return this.host;
915     }
916 
917     /**
918      * Gets the port.
919      *
920      * @return  the port.
921      */
922     public int getPort() {
923         return this.port;
924     }
925 
926     /**
927      * Tests whether the path is empty.
928      *
929      * @return whether the path is empty.
930      */
931     public boolean isPathEmpty() {
932         return (this.pathSegments == null || this.pathSegments.isEmpty()) &&
933                 (this.encodedPath == null || this.encodedPath.isEmpty());
934     }
935 
936     /**
937      * Gets the path segments.
938      *
939      * @return the path segments.
940      */
941     public List<String> getPathSegments() {
942         return this.pathSegments != null ? new ArrayList<>(this.pathSegments) : new ArrayList<>();
943     }
944 
945     /**
946      * Gets the path.
947      *
948      * @return the path.
949      */
950     public String getPath() {
951         if (this.pathSegments == null) {
952             return null;
953         }
954         final StringBuilder result = new StringBuilder();
955         for (final String segment : this.pathSegments) {
956             result.append('/').append(segment);
957         }
958         return result.toString();
959     }
960 
961     /**
962      * Tests whether the query is empty.
963      *
964      * @return whether the query is empty.
965      */
966     public boolean isQueryEmpty() {
967         return (this.queryParams == null || this.queryParams.isEmpty()) && this.encodedQuery == null;
968     }
969 
970     /**
971      * Gets the query parameters as a List.
972      *
973      * @return the query parameters as a List.
974      */
975     public List<NameValuePair> getQueryParams() {
976         return this.queryParams != null ? new ArrayList<>(this.queryParams) : new ArrayList<>();
977     }
978 
979     /**
980      * Gets the first {@link NameValuePair} for a given name.
981      *
982      * @param name the name
983      * @return the first named {@link NameValuePair} or null if not found.
984      * @since 5.2
985      */
986     public NameValuePair getFirstQueryParam(final String name) {
987         return queryParams.stream().filter(e -> name.equals(e.getName())).findFirst().orElse(null);
988     }
989 
990     /**
991      * Gets the fragments.
992      *
993      * @return the fragments.
994      */
995     public String getFragment() {
996         return this.fragment;
997     }
998 
999     /**
1000      * Normalizes syntax of URI components if the URI is considered non-opaque
1001      * (the path component has a root):
1002      * <ul>
1003      *  <li>characters of scheme and host components are converted to lower case</li>
1004      *  <li>dot segments of the path component are removed if the path has a root</li>
1005      *  <li>percent encoding of all components is normalized</li>
1006      * </ul>
1007      *
1008      * @since 5.1
1009      */
1010     public URIBuilder normalizeSyntax() {
1011         final String scheme = this.scheme;
1012         if (scheme != null) {
1013             this.scheme = TextUtils.toLowerCase(scheme);
1014         }
1015 
1016         if (this.pathRootless) {
1017             return this;
1018         }
1019 
1020         // Force Percent-Encoding normalization
1021         this.encodedSchemeSpecificPart = null;
1022         this.encodedAuthority = null;
1023         this.encodedUserInfo = null;
1024         this.encodedPath = null;
1025         this.encodedQuery = null;
1026         this.encodedFragment = null;
1027 
1028         final String host = this.host;
1029         if (host != null) {
1030             this.host = TextUtils.toLowerCase(host);
1031         }
1032 
1033         if (this.pathSegments != null) {
1034             final List<String> inputSegments = this.pathSegments;
1035             if (!inputSegments.isEmpty()) {
1036                 final LinkedList<String> outputSegments = new LinkedList<>();
1037                 for (final String inputSegment : inputSegments) {
1038                     if (!inputSegment.isEmpty() && !".".equals(inputSegment)) {
1039                         if ("..".equals(inputSegment)) {
1040                             if (!outputSegments.isEmpty()) {
1041                                 outputSegments.removeLast();
1042                             }
1043                         } else {
1044                             outputSegments.addLast(inputSegment);
1045                         }
1046                     }
1047                 }
1048                 if (!inputSegments.isEmpty()) {
1049                     final String lastSegment = inputSegments.get(inputSegments.size() - 1);
1050                     if (lastSegment.isEmpty()) {
1051                         outputSegments.addLast("");
1052                     }
1053                 }
1054                 this.pathSegments = outputSegments;
1055             } else {
1056                 this.pathSegments = Collections.singletonList("");
1057             }
1058         }
1059 
1060         return this;
1061     }
1062 
1063     /**
1064      * Converts this instance to a URI string.
1065      *
1066      * @return this instance to a URI string.
1067      */
1068     @Override
1069     public String toString() {
1070         return buildString();
1071     }
1072 
1073 }