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