001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027package org.apache.http.client.utils;
028
029import java.net.URI;
030import java.net.URISyntaxException;
031import java.nio.charset.Charset;
032import java.util.ArrayList;
033import java.util.Iterator;
034import java.util.List;
035
036import org.apache.http.Consts;
037import org.apache.http.NameValuePair;
038import org.apache.http.conn.util.InetAddressUtils;
039import org.apache.http.message.BasicNameValuePair;
040
041/**
042 * Builder for {@link URI} instances.
043 *
044 * @since 4.2
045 */
046public class URIBuilder {
047
048    private String scheme;
049    private String encodedSchemeSpecificPart;
050    private String encodedAuthority;
051    private String userInfo;
052    private String encodedUserInfo;
053    private String host;
054    private int port;
055    private String path;
056    private String encodedPath;
057    private String encodedQuery;
058    private List<NameValuePair> queryParams;
059    private String query;
060    private Charset charset;
061    private String fragment;
062    private String encodedFragment;
063
064    /**
065     * Constructs an empty instance.
066     */
067    public URIBuilder() {
068        super();
069        this.port = -1;
070    }
071
072    /**
073     * Construct an instance from the string which must be a valid URI.
074     *
075     * @param string a valid URI in string form
076     * @throws URISyntaxException if the input is not a valid URI
077     */
078    public URIBuilder(final String string) throws URISyntaxException {
079        super();
080        digestURI(new URI(string));
081    }
082
083    /**
084     * Construct an instance from the provided URI.
085     * @param uri
086     */
087    public URIBuilder(final URI uri) {
088        super();
089        digestURI(uri);
090    }
091
092    /**
093     * @since 4.4
094     */
095    public URIBuilder setCharset(final Charset charset) {
096        this.charset = charset;
097        return this;
098    }
099
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}