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 = scheme;
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 = userInfo;
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.getHostAddress();
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 = host;
285         this.encodedSchemeSpecificPart = null;
286         this.encodedAuthority = null;
287         return this;
288     }
289 
290     /**
291      * Sets URI port.
292      */
293     public URIBuilder setPort(final int port) {
294         this.port = port < 0 ? -1 : port;
295         this.encodedSchemeSpecificPart = null;
296         this.encodedAuthority = null;
297         return this;
298     }
299 
300     /**
301      * Sets URI path. The value is expected to be unescaped and may contain non ASCII characters.
302      */
303     public URIBuilder setPath(final String path) {
304         this.path = path;
305         this.encodedSchemeSpecificPart = null;
306         this.encodedPath = null;
307         return this;
308     }
309 
310     /**
311      * Removes URI query.
312      */
313     public URIBuilder removeQuery() {
314         this.queryParams = null;
315         this.query = null;
316         this.encodedQuery = null;
317         this.encodedSchemeSpecificPart = null;
318         return this;
319     }
320 
321     /**
322      * Sets URI query parameters. The parameter name / values are expected to be unescaped
323      * and may contain non ASCII characters.
324      * <p>
325      * Please note query parameters and custom query component are mutually exclusive. This method
326      * will remove custom query if present.
327      * </p>
328      *
329      * @since 4.3
330      */
331     public URIBuilder setParameters(final List <NameValuePair> nvps) {
332         if (this.queryParams == null) {
333             this.queryParams = new ArrayList<>();
334         } else {
335             this.queryParams.clear();
336         }
337         this.queryParams.addAll(nvps);
338         this.encodedQuery = null;
339         this.encodedSchemeSpecificPart = null;
340         this.query = null;
341         return this;
342     }
343 
344     /**
345      * Adds URI query parameters. The parameter name / values are expected to be unescaped
346      * and may contain non ASCII characters.
347      * <p>
348      * Please note query parameters and custom query component are mutually exclusive. This method
349      * will remove custom query if present.
350      * </p>
351      *
352      * @since 4.3
353      */
354     public URIBuilder addParameters(final List <NameValuePair> nvps) {
355         if (this.queryParams == null) {
356             this.queryParams = new ArrayList<>();
357         }
358         this.queryParams.addAll(nvps);
359         this.encodedQuery = null;
360         this.encodedSchemeSpecificPart = null;
361         this.query = null;
362         return this;
363     }
364 
365     /**
366      * Sets URI query parameters. The parameter name / values are expected to be unescaped
367      * and may contain non ASCII characters.
368      * <p>
369      * Please note query parameters and custom query component are mutually exclusive. This method
370      * will remove custom query if present.
371      * </p>
372      *
373      * @since 4.3
374      */
375     public URIBuilder setParameters(final NameValuePair... nvps) {
376         if (this.queryParams == null) {
377             this.queryParams = new ArrayList<>();
378         } else {
379             this.queryParams.clear();
380         }
381         for (final NameValuePair nvp: nvps) {
382             this.queryParams.add(nvp);
383         }
384         this.encodedQuery = null;
385         this.encodedSchemeSpecificPart = null;
386         this.query = null;
387         return this;
388     }
389 
390     /**
391      * Adds parameter to URI query. The parameter name and value are expected to be unescaped
392      * and may contain non ASCII characters.
393      * <p>
394      * Please note query parameters and custom query component are mutually exclusive. This method
395      * will remove custom query if present.
396      * </p>
397      */
398     public URIBuilder addParameter(final String param, final String value) {
399         if (this.queryParams == null) {
400             this.queryParams = new ArrayList<>();
401         }
402         this.queryParams.add(new BasicNameValuePair(param, value));
403         this.encodedQuery = null;
404         this.encodedSchemeSpecificPart = null;
405         this.query = null;
406         return this;
407     }
408 
409     /**
410      * Sets parameter of URI query overriding existing value if set. The parameter name and value
411      * are expected to be unescaped and may contain non ASCII characters.
412      * <p>
413      * Please note query parameters and custom query component are mutually exclusive. This method
414      * will remove custom query if present.
415      * </p>
416      */
417     public URIBuilder setParameter(final String param, final String value) {
418         if (this.queryParams == null) {
419             this.queryParams = new ArrayList<>();
420         }
421         if (!this.queryParams.isEmpty()) {
422             for (final Iterator<NameValuePair> it = this.queryParams.iterator(); it.hasNext(); ) {
423                 final NameValuePair nvp = it.next();
424                 if (nvp.getName().equals(param)) {
425                     it.remove();
426                 }
427             }
428         }
429         this.queryParams.add(new BasicNameValuePair(param, value));
430         this.encodedQuery = null;
431         this.encodedSchemeSpecificPart = null;
432         this.query = null;
433         return this;
434     }
435 
436     /**
437      * Clears URI query parameters.
438      *
439      * @since 4.3
440      */
441     public URIBuilder clearParameters() {
442         this.queryParams = null;
443         this.encodedQuery = null;
444         this.encodedSchemeSpecificPart = null;
445         return this;
446     }
447 
448     /**
449      * Sets custom URI query. The value is expected to be unescaped and may contain non ASCII
450      * characters.
451      * <p>
452      * Please note query parameters and custom query component are mutually exclusive. This method
453      * will remove query parameters if present.
454      * </p>
455      *
456      * @since 4.3
457      */
458     public URIBuilder setCustomQuery(final String query) {
459         this.query = query;
460         this.encodedQuery = null;
461         this.encodedSchemeSpecificPart = null;
462         this.queryParams = null;
463         return this;
464     }
465 
466     /**
467      * Sets URI fragment. The value is expected to be unescaped and may contain non ASCII
468      * characters.
469      */
470     public URIBuilder setFragment(final String fragment) {
471         this.fragment = fragment;
472         this.encodedFragment = null;
473         return this;
474     }
475 
476     /**
477      * @since 4.3
478      */
479     public boolean isAbsolute() {
480         return this.scheme != null;
481     }
482 
483     /**
484      * @since 4.3
485      */
486     public boolean isOpaque() {
487         return this.path == null;
488     }
489 
490     public String getScheme() {
491         return this.scheme;
492     }
493 
494     public String getUserInfo() {
495         return this.userInfo;
496     }
497 
498     public String getHost() {
499         return this.host;
500     }
501 
502     public int getPort() {
503         return this.port;
504     }
505 
506     public String getPath() {
507         return this.path;
508     }
509 
510     public List<NameValuePair> getQueryParams() {
511         if (this.queryParams != null) {
512             return new ArrayList<>(this.queryParams);
513         } else {
514             return new ArrayList<>();
515         }
516     }
517 
518     public String getFragment() {
519         return this.fragment;
520     }
521 
522     @Override
523     public String toString() {
524         return buildString();
525     }
526 
527 }