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}