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.Iterator;
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.message.BasicNameValuePair;
44  import org.apache.hc.core5.util.TextUtils;
45  
46  /**
47   * Builder for {@link URI} instances.
48   *
49   * @since 5.0
50   */
51  public class URIBuilder {
52  
53      /**
54       * Creates a new builder for the host {@link InetAddress#getLocalHost()}.
55       *
56       * @return a new builder.
57       * @throws UnknownHostException if the local host name could not be resolved into an address.
58       */
59      public static URIBuilder localhost() throws UnknownHostException {
60          return new URIBuilder().setHost(InetAddress.getLocalHost());
61      }
62  
63      /**
64       * Creates a new builder for the host {@link InetAddress#getLoopbackAddress()}.
65       */
66      public static URIBuilder loopbackAddress() {
67          return new URIBuilder().setHost(InetAddress.getLoopbackAddress());
68      }
69  
70      private String scheme;
71      private String encodedSchemeSpecificPart;
72      private String encodedAuthority;
73      private String userInfo;
74      private String encodedUserInfo;
75      private String host;
76      private int port;
77      private String encodedPath;
78      private List<String> pathSegments;
79      private String encodedQuery;
80      private List<NameValuePair> queryParams;
81      private String query;
82      private Charset charset;
83      private String fragment;
84      private String encodedFragment;
85  
86      /**
87       * Constructs an empty instance.
88       */
89      public URIBuilder() {
90          super();
91          this.port = -1;
92      }
93  
94      /**
95       * Construct an instance from the string which must be a valid URI.
96       *
97       * @param string a valid URI in string form
98       * @throws URISyntaxException if the input is not a valid URI
99       */
100     public URIBuilder(final String string) throws URISyntaxException {
101         this(new URI(string), null);
102     }
103 
104     /**
105      * Construct an instance from the provided URI.
106      * @param uri
107      */
108     public URIBuilder(final URI uri) {
109         this(uri, null);
110     }
111 
112     /**
113      * Construct an instance from the string which must be a valid URI.
114      *
115      * @param string a valid URI in string form
116      * @throws URISyntaxException if the input is not a valid URI
117      */
118     public URIBuilder(final String string, final Charset charset) throws URISyntaxException {
119         this(new URI(string), charset);
120     }
121 
122     /**
123      * Construct an instance from the provided URI.
124      * @param uri
125      */
126     public URIBuilder(final URI uri, final Charset charset) {
127         super();
128         setCharset(charset);
129         digestURI(uri);
130     }
131 
132     public URIBuilder setCharset(final Charset charset) {
133         this.charset = charset;
134         return this;
135     }
136 
137     public Charset getCharset() {
138         return charset;
139     }
140 
141     private List <NameValuePair> parseQuery(final String query, final Charset charset) {
142         if (query != null && !query.isEmpty()) {
143             return URLEncodedUtils.parse(query, charset);
144         }
145         return null;
146     }
147 
148     private List <String> parsePath(final String path, final Charset charset) {
149         if (path != null && !path.isEmpty()) {
150             return URLEncodedUtils.parsePathSegments(path, charset);
151         }
152         return null;
153     }
154 
155     /**
156      * Builds a {@link URI} instance.
157      */
158     public URI build() throws URISyntaxException {
159         return new URI(buildString());
160     }
161 
162     private String buildString() {
163         final StringBuilder sb = new StringBuilder();
164         if (this.scheme != null) {
165             sb.append(this.scheme).append(':');
166         }
167         if (this.encodedSchemeSpecificPart != null) {
168             sb.append(this.encodedSchemeSpecificPart);
169         } else {
170             if (this.encodedAuthority != null) {
171                 sb.append("//").append(this.encodedAuthority);
172             } else if (this.host != null) {
173                 sb.append("//");
174                 if (this.encodedUserInfo != null) {
175                     sb.append(this.encodedUserInfo).append("@");
176                 } else if (this.userInfo != null) {
177                     encodeUserInfo(sb, this.userInfo);
178                     sb.append("@");
179                 }
180                 if (InetAddressUtils.isIPv6Address(this.host)) {
181                     sb.append("[").append(this.host).append("]");
182                 } else {
183                     sb.append(this.host);
184                 }
185                 if (this.port >= 0) {
186                     sb.append(":").append(this.port);
187                 }
188             }
189             if (this.encodedPath != null) {
190                 sb.append(normalizePath(this.encodedPath, sb.length() == 0));
191             } else if (this.pathSegments != null) {
192                 encodePath(sb, this.pathSegments);
193             }
194             if (this.encodedQuery != null) {
195                 sb.append("?").append(this.encodedQuery);
196             } else if (this.queryParams != null && !this.queryParams.isEmpty()) {
197                 sb.append("?");
198                 encodeUrlForm(sb, this.queryParams);
199             } else if (this.query != null) {
200                 sb.append("?");
201                 encodeUric(sb, this.query);
202             }
203         }
204         if (this.encodedFragment != null) {
205             sb.append("#").append(this.encodedFragment);
206         } else if (this.fragment != null) {
207             sb.append("#");
208             encodeUric(sb, this.fragment);
209         }
210         return sb.toString();
211     }
212 
213     private static String normalizePath(final String path, final boolean relative) {
214         String s = path;
215         if (TextUtils.isBlank(s)) {
216             return "";
217         }
218         if (!relative && !s.startsWith("/")) {
219             s = "/" + s;
220         }
221         return s;
222     }
223 
224     private void digestURI(final URI uri) {
225         this.scheme = uri.getScheme();
226         this.encodedSchemeSpecificPart = uri.getRawSchemeSpecificPart();
227         this.encodedAuthority = uri.getRawAuthority();
228         this.host = uri.getHost();
229         this.port = uri.getPort();
230         this.encodedUserInfo = uri.getRawUserInfo();
231         this.userInfo = uri.getUserInfo();
232         this.encodedPath = uri.getRawPath();
233         this.pathSegments = parsePath(uri.getRawPath(), this.charset != null ? this.charset : StandardCharsets.UTF_8);
234         this.encodedQuery = uri.getRawQuery();
235         this.queryParams = parseQuery(uri.getRawQuery(), this.charset != null ? this.charset : StandardCharsets.UTF_8);
236         this.encodedFragment = uri.getRawFragment();
237         this.fragment = uri.getFragment();
238     }
239 
240     private void encodeUserInfo(final StringBuilder buf, final String userInfo) {
241         URLEncodedUtils.encUserInfo(buf, userInfo, this.charset != null ? this.charset : StandardCharsets.UTF_8);
242     }
243 
244     private void encodePath(final StringBuilder buf, final List<String> pathSegments) {
245         URLEncodedUtils.formatSegments(buf, pathSegments, this.charset != null ? this.charset : StandardCharsets.UTF_8);
246     }
247 
248     private void encodeUrlForm(final StringBuilder buf, final List<NameValuePair> params) {
249         URLEncodedUtils.formatParameters(buf, params, this.charset != null ? this.charset : StandardCharsets.UTF_8);
250     }
251 
252     private void encodeUric(final StringBuilder buf, final String fragment) {
253         URLEncodedUtils.encUric(buf, fragment, this.charset != null ? this.charset : StandardCharsets.UTF_8);
254     }
255 
256     /**
257      * Sets URI scheme.
258      *
259      * @return this.
260      */
261     public URIBuilder setScheme(final String scheme) {
262         this.scheme = !TextUtils.isBlank(scheme) ? scheme : null;
263         return this;
264     }
265 
266     /**
267      * Sets URI user info. The value is expected to be unescaped and may contain non ASCII
268      * characters.
269      *
270      * @return this.
271      */
272     public URIBuilder setUserInfo(final String userInfo) {
273         this.userInfo = !TextUtils.isBlank(userInfo) ? userInfo : null;
274         this.encodedSchemeSpecificPart = null;
275         this.encodedAuthority = null;
276         this.encodedUserInfo = null;
277         return this;
278     }
279 
280     /**
281      * Sets URI user info as a combination of username and password. These values are expected to
282      * be unescaped and may contain non ASCII characters.
283      *
284      * @return this.
285      */
286     public URIBuilder setUserInfo(final String username, final String password) {
287         return setUserInfo(username + ':' + password);
288     }
289 
290     /**
291      * Sets URI host.
292      *
293      * @return this.
294      */
295     public URIBuilder setHost(final InetAddress host) {
296         this.host = host != null ? host.getHostAddress() : null;
297         this.encodedSchemeSpecificPart = null;
298         this.encodedAuthority = null;
299         return this;
300     }
301 
302     /**
303      * Sets URI host.
304      *
305      * @return this.
306      */
307     public URIBuilder setHost(final String host) {
308         this.host = !TextUtils.isBlank(host) ? host : null;
309         this.encodedSchemeSpecificPart = null;
310         this.encodedAuthority = null;
311         return this;
312     }
313 
314     /**
315      * Sets the scheme, host name, and port.
316      *
317      * @param httpHost the scheme, host name, and port.
318      * @return this.
319      */
320     public URIBuilder setHttpHost(final HttpHost httpHost ) {
321         setScheme(httpHost.getSchemeName());
322         setHost(httpHost.getHostName());
323         setPort(httpHost.getPort());
324         return this;
325     }
326 
327     /**
328      * Sets URI port.
329      *
330      * @return this.
331      */
332     public URIBuilder setPort(final int port) {
333         this.port = port < 0 ? -1 : port;
334         this.encodedSchemeSpecificPart = null;
335         this.encodedAuthority = null;
336         return this;
337     }
338 
339     /**
340      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
341      *
342      * @return this.
343      */
344     public URIBuilder setPath(final String path) {
345         return setPathSegments(path != null ? URLEncodedUtils.splitPathSegments(path) : null);
346     }
347 
348     /**
349      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
350      *
351      * @return this.
352      */
353     public URIBuilder setPathSegments(final String... pathSegments) {
354         this.pathSegments = pathSegments.length > 0 ? Arrays.asList(pathSegments) : null;
355         this.encodedSchemeSpecificPart = null;
356         this.encodedPath = null;
357         return this;
358     }
359 
360     /**
361      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
362      *
363      * @return this.
364      */
365     public URIBuilder setPathSegments(final List<String> pathSegments) {
366         this.pathSegments = pathSegments != null && pathSegments.size() > 0 ? new ArrayList<>(pathSegments) : null;
367         this.encodedSchemeSpecificPart = null;
368         this.encodedPath = null;
369         return this;
370     }
371 
372     /**
373      * Removes URI query.
374      *
375      * @return this.
376      */
377     public URIBuilder removeQuery() {
378         this.queryParams = null;
379         this.query = null;
380         this.encodedQuery = null;
381         this.encodedSchemeSpecificPart = null;
382         return this;
383     }
384 
385     /**
386      * Sets URI query parameters. The parameter name / values are expected to be unescaped
387      * and may contain non ASCII characters.
388      * <p>
389      * Please note query parameters and custom query component are mutually exclusive. This method
390      * will remove custom query if present.
391      * </p>
392      *
393      * @return this.
394      */
395     public URIBuilder setParameters(final List <NameValuePair> nvps) {
396         if (this.queryParams == null) {
397             this.queryParams = new ArrayList<>();
398         } else {
399             this.queryParams.clear();
400         }
401         this.queryParams.addAll(nvps);
402         this.encodedQuery = null;
403         this.encodedSchemeSpecificPart = null;
404         this.query = null;
405         return this;
406     }
407 
408     /**
409      * Adds URI query parameters. The parameter name / values are expected to be unescaped
410      * and may contain non ASCII characters.
411      * <p>
412      * Please note query parameters and custom query component are mutually exclusive. This method
413      * will remove custom query if present.
414      * </p>
415      *
416      * @return this.
417      */
418     public URIBuilder addParameters(final List <NameValuePair> nvps) {
419         if (this.queryParams == null) {
420             this.queryParams = new ArrayList<>();
421         }
422         this.queryParams.addAll(nvps);
423         this.encodedQuery = null;
424         this.encodedSchemeSpecificPart = null;
425         this.query = null;
426         return this;
427     }
428 
429     /**
430      * Sets URI query parameters. The parameter name / values are expected to be unescaped
431      * and may contain non ASCII characters.
432      * <p>
433      * Please note query parameters and custom query component are mutually exclusive. This method
434      * will remove custom query if present.
435      * </p>
436      *
437      * @return this.
438      */
439     public URIBuilder setParameters(final NameValuePair... nvps) {
440         if (this.queryParams == null) {
441             this.queryParams = new ArrayList<>();
442         } else {
443             this.queryParams.clear();
444         }
445         Collections.addAll(this.queryParams, nvps);
446         this.encodedQuery = null;
447         this.encodedSchemeSpecificPart = null;
448         this.query = null;
449         return this;
450     }
451 
452     /**
453      * Adds parameter to URI query. The parameter name and value are expected to be unescaped
454      * and may contain non ASCII characters.
455      * <p>
456      * Please note query parameters and custom query component are mutually exclusive. This method
457      * will remove custom query if present.
458      * </p>
459      *
460      * @return this.
461      */
462     public URIBuilder addParameter(final String param, final String value) {
463         if (this.queryParams == null) {
464             this.queryParams = new ArrayList<>();
465         }
466         this.queryParams.add(new BasicNameValuePair(param, value));
467         this.encodedQuery = null;
468         this.encodedSchemeSpecificPart = null;
469         this.query = null;
470         return this;
471     }
472 
473     /**
474      * Sets parameter of URI query overriding existing value if set. The parameter name and value
475      * are expected to be unescaped and may contain non ASCII characters.
476      * <p>
477      * Please note query parameters and custom query component are mutually exclusive. This method
478      * will remove custom query if present.
479      * </p>
480      *
481      * @return this.
482      */
483     public URIBuilder setParameter(final String param, final String value) {
484         if (this.queryParams == null) {
485             this.queryParams = new ArrayList<>();
486         }
487         if (!this.queryParams.isEmpty()) {
488             for (final Iterator<NameValuePair> it = this.queryParams.iterator(); it.hasNext(); ) {
489                 final NameValuePair nvp = it.next();
490                 if (nvp.getName().equals(param)) {
491                     it.remove();
492                 }
493             }
494         }
495         this.queryParams.add(new BasicNameValuePair(param, value));
496         this.encodedQuery = null;
497         this.encodedSchemeSpecificPart = null;
498         this.query = null;
499         return this;
500     }
501 
502     /**
503      * Clears URI query parameters.
504      *
505      * @return this.
506      */
507     public URIBuilder clearParameters() {
508         this.queryParams = null;
509         this.encodedQuery = null;
510         this.encodedSchemeSpecificPart = null;
511         return this;
512     }
513 
514     /**
515      * Sets custom URI query. The value is expected to be unescaped and may contain non ASCII
516      * characters.
517      * <p>
518      * Please note query parameters and custom query component are mutually exclusive. This method
519      * will remove query parameters if present.
520      * </p>
521      *
522      * @return this.
523      */
524     public URIBuilder setCustomQuery(final String query) {
525         this.query = !TextUtils.isBlank(query) ? query : null;
526         this.encodedQuery = null;
527         this.encodedSchemeSpecificPart = null;
528         this.queryParams = null;
529         return this;
530     }
531 
532     /**
533      * Sets URI fragment. The value is expected to be unescaped and may contain non ASCII
534      * characters.
535      *
536      * @return this.
537      */
538     public URIBuilder setFragment(final String fragment) {
539         this.fragment = !TextUtils.isBlank(fragment) ? fragment : null;
540         this.encodedFragment = null;
541         return this;
542     }
543 
544     public boolean isAbsolute() {
545         return this.scheme != null;
546     }
547 
548     public boolean isOpaque() {
549         return this.pathSegments == null && this.encodedPath == null;
550     }
551 
552     public String getScheme() {
553         return this.scheme;
554     }
555 
556     public String getUserInfo() {
557         return this.userInfo;
558     }
559 
560     public String getHost() {
561         return this.host;
562     }
563 
564     public int getPort() {
565         return this.port;
566     }
567 
568     public boolean isPathEmpty() {
569         return (this.pathSegments == null || this.pathSegments.isEmpty()) &&
570                 (this.encodedPath == null || this.encodedPath.isEmpty());
571     }
572 
573     public List<String> getPathSegments() {
574         return this.pathSegments != null ? new ArrayList<>(this.pathSegments) : Collections.<String>emptyList();
575     }
576 
577     public String getPath() {
578         if (this.pathSegments == null) {
579             return null;
580         }
581         final StringBuilder result = new StringBuilder();
582         for (final String segment : this.pathSegments) {
583             result.append('/').append(segment);
584         }
585         return result.toString();
586     }
587 
588     public boolean isQueryEmpty() {
589         return (this.queryParams == null || this.queryParams.isEmpty()) && this.encodedQuery == null;
590     }
591 
592     public List<NameValuePair> getQueryParams() {
593         return this.queryParams != null ? new ArrayList<>(this.queryParams) : Collections.<NameValuePair>emptyList();
594     }
595 
596     public String getFragment() {
597         return this.fragment;
598     }
599 
600     @Override
601     public String toString() {
602         return buildString();
603     }
604 
605 }