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