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