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