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