001/* 002 * Copyright 2008-2011 Thomas Nichols. http://blog.thomnichols.org 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * You are receiving this code free of charge, which represents many hours of 017 * effort from other individuals and corporations. As a responsible member 018 * of the community, you are encouraged (but not required) to donate any 019 * enhancements or improvements back to the community under a similar open 020 * source license. Thank you. -TMN 021 */ 022package groovyx.net.http; 023 024import java.net.MalformedURLException; 025import java.net.URI; 026import java.net.URISyntaxException; 027import java.net.URL; 028import java.util.ArrayList; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032 033import org.apache.http.NameValuePair; 034import org.apache.http.client.utils.URLEncodedUtils; 035import org.apache.http.message.BasicNameValuePair; 036 037/** 038 * This class implements a mutable URI. All <code>set</code>, <code>add</code> 039 * and <code>remove</code> methods affect this class' internal URI 040 * representation. All mutator methods support chaining, e.g. 041 * <pre> 042 * new URIBuilder("http://www.google.com/") 043 * .setScheme( "https" ) 044 * .setPort( 443 ) 045 * .setPath( "some/path" ) 046 * .toString(); 047 * </pre> 048 * A slightly more 'Groovy' version would be: 049 * <pre> 050 * new URIBuilder('http://www.google.com/').with { 051 * scheme = 'https' 052 * port = 443 053 * path = 'some/path' 054 * query = [p1:1, p2:'two'] 055 * return it 056 * }.toString() 057 * </pre> 058 * @author <a href='mailto:tomstrummer+httpbuilder@gmail.com'>Tom Nichols</a> 059 */ 060public class URIBuilder implements Cloneable { 061 protected URI base; 062 private final String ENC = "UTF-8"; 063 064 public URIBuilder( String url ) throws URISyntaxException { 065 base = new URI(url); 066 } 067 068 public URIBuilder( URL url ) throws URISyntaxException { 069 this.base = url.toURI(); 070 } 071 072 /** 073 * @throws IllegalArgumentException if uri is null 074 * @param uri 075 */ 076 public URIBuilder( URI uri ) throws IllegalArgumentException { 077 if ( uri == null ) 078 throw new IllegalArgumentException( "uri cannot be null" ); 079 this.base = uri; 080 } 081 082 /** 083 * Utility method to convert a number of type to a URI instance. 084 * @param uri a {@link URI}, {@link URL} or any object that produces a 085 * valid URI string from its <code>toString()</code> result. 086 * @return a valid URI parsed from the given object 087 * @throws URISyntaxException 088 */ 089 public static URI convertToURI( Object uri ) throws URISyntaxException { 090 if ( uri instanceof URI ) return (URI)uri; 091 if ( uri instanceof URL ) return ((URL)uri).toURI(); 092 if ( uri instanceof URIBuilder ) return ((URIBuilder)uri).toURI(); 093 return new URI( uri.toString() ); // assume any other object type produces a valid URI string 094 } 095 096 protected URI update( String scheme, String userInfo, String host, int port, 097 String path, String query, String fragment ) throws URISyntaxException { 098 URI u = new URI( scheme, userInfo, host, port, base.getPath(), null, null ); 099 100 StringBuilder sb = new StringBuilder(); 101 if ( path != null ) sb.append( path ); 102 if ( query != null ) 103 sb.append( '?' ).append( query ); 104 if ( fragment != null ) sb.append( '#' ).append( fragment ); 105 return u.resolve( sb.toString() ); 106 } 107 108 /** 109 * Set the URI scheme, AKA the 'protocol.' e.g. 110 * <code>setScheme('https')</code> 111 * @throws URISyntaxException if the given scheme contains illegal characters. 112 */ 113 public URIBuilder setScheme( String scheme ) throws URISyntaxException { 114 this.base = update( scheme, base.getUserInfo(), 115 base.getHost(), base.getPort(), 116 base.getRawPath(), base.getRawQuery(), base.getRawFragment() ); 117 return this; 118 } 119 120 /** 121 * Get the scheme for this URI. See {@link URI#getScheme()} 122 * @return the scheme portion of the URI 123 */ 124 public String getScheme() { 125 return this.base.getScheme(); 126 } 127 128 /** 129 * Set the port for this URI, or <code>-1</code> to unset the port. 130 * @param port 131 * @return this URIBuilder instance 132 * @throws URISyntaxException 133 */ 134 public URIBuilder setPort( int port ) throws URISyntaxException { 135 this.base = update( base.getScheme(), base.getUserInfo(), 136 base.getHost(), port, base.getRawPath(), 137 base.getRawQuery(), base.getRawFragment() ); 138 return this; 139 } 140 141 /** 142 * See {@link URI#getPort()} 143 * @return the port portion of this URI (-1 if a port is not specified.) 144 */ 145 public int getPort() { 146 return this.base.getPort(); 147 } 148 149 /** 150 * Set the host portion of this URI. 151 * @param host 152 * @return this URIBuilder instance 153 * @throws URISyntaxException if the host parameter contains illegal characters. 154 */ 155 public URIBuilder setHost( String host ) throws URISyntaxException { 156 this.base = update( base.getScheme(), base.getUserInfo(), 157 host, base.getPort(), base.getRawPath(), 158 base.getRawQuery(), base.getRawFragment() ); 159 return this; 160 } 161 162 /** 163 * See {@link URI#getHost()} 164 * @return the host portion of the URI 165 */ 166 public String getHost() { 167 return base.getHost(); 168 } 169 170 /** 171 * Set the path component of this URI. The value may be absolute or 172 * relative to the current path. 173 * e.g. <pre> 174 * def uri = new URIBuilder( 'http://localhost/p1/p2?a=1' ) 175 * 176 * uri.path = '/p3/p2' 177 * assert uri.toString() == 'http://localhost/p3/p2?a=1' 178 * 179 * uri.path = 'p2a' 180 * assert uri.toString() == 'http://localhost/p3/p2a?a=1' 181 * 182 * uri.path = '../p4' 183 * assert uri.toString() == 'http://localhost/p4?a=1&b=2&c=3#frag' 184 * <pre> 185 * @param path the path portion of this URI, relative to the current URI. 186 * @return this URIBuilder instance, for method chaining. 187 * @throws URISyntaxException if the given path contains characters that 188 * cannot be converted to a valid URI 189 */ 190 public URIBuilder setPath( String path ) throws URISyntaxException { 191 this.base = update( base.getScheme(), base.getUserInfo(), 192 base.getHost(), base.getPort(), 193 new URI( null, null, path, null, null ).getRawPath(), 194 base.getRawQuery(), base.getRawFragment() ); 195 return this; 196 } 197 198 /** 199 * Note that this property is <strong>not</strong> necessarily reflexive 200 * with the {@link #setPath(String)} method! <code>URIBuilder.setPath()</code> 201 * will resolve a relative path, whereas this method will always return the 202 * full, absolute path. 203 * See {@link URI#getPath()} 204 * @return the full path portion of the URI. 205 */ 206 public String getPath() { 207 return this.base.getPath(); 208 } 209 210 /* TODO null/ zero-size check if this is ever made public */ 211 protected URIBuilder setQueryNVP( List<NameValuePair> nvp ) throws URISyntaxException { 212 /* Passing the query string in the URI constructor will 213 * double-escape query parameters and goober things up. So we have 214 * to create a full path+query+fragment and use URI#resolve() to 215 * create the new URI. */ 216 StringBuilder sb = new StringBuilder(); 217 String path = base.getRawPath(); 218 if ( path != null ) sb.append( path ); 219 sb.append( '?' ); 220 sb.append( URLEncodedUtils.format( nvp, ENC ) ); 221 String frag = base.getRawFragment(); 222 if ( frag != null ) sb.append( '#' ).append( frag ); 223 this.base = base.resolve( sb.toString() ); 224 225 return this; 226 } 227 228 /** 229 * Set the query portion of the URI. For query parameters with multiple 230 * values, put the values in a list like so: 231 * <pre>uri.query = [ p1:'val1', p2:['val2', 'val3'] ] 232 * // will produce a query string of ?p1=val1&p2=val2&p2=val3</pre> 233 * 234 * @param params a Map of parameters that will be transformed into the query string 235 * @return this URIBuilder instance, for method chaining. 236 * @throws URISyntaxException 237 */ 238 public URIBuilder setQuery( Map<?,?> params ) throws URISyntaxException { 239 if ( params == null || params.size() < 1 ) { 240 this.base = new URI( base.getScheme(), base.getUserInfo(), 241 base.getHost(), base.getPort(), base.getPath(), 242 null, base.getFragment() ); 243 } 244 else { 245 List<NameValuePair> nvp = new ArrayList<NameValuePair>(params.size()); 246 for ( Object key : params.keySet() ) { 247 Object value = params.get(key); 248 if ( value instanceof List<?> ) { 249 for (Object val : (List<?>)value ) 250 nvp.add( new BasicNameValuePair( key.toString(), 251 ( val != null ) ? val.toString() : "" ) ); 252 } 253 else nvp.add( new BasicNameValuePair( key.toString(), 254 ( value != null ) ? value.toString() : "" ) ); 255 } 256 this.setQueryNVP( nvp ); 257 } 258 return this; 259 } 260 261 /** 262 * Set the raw, already-escaped query string. No additional escaping will 263 * be done on the string. 264 * @param query 265 * @return 266 */ 267 public URIBuilder setRawQuery( String query ) throws URISyntaxException { 268 this.base = update( base.getScheme(), base.getUserInfo(), 269 base.getHost(), base.getPort(), 270 base.getRawPath(), query, base.getRawFragment() ); 271 return this; 272 } 273 274 /** 275 * Get the query string as a map for convenience. If any parameter contains 276 * multiple values (e.g. <code>p1=one&p1=two</code>) both values will be 277 * inserted into a list for that paramter key (<code>[p1 : ['one','two']] 278 * </code>). Note that this is not a "live" map. Therefore, you cannot 279 * call 280 * <pre> uri.query.a = 'BCD'</pre> 281 * You will not modify the query string but instead the generated map of 282 * parameters. Instead, you need to use {@link #removeQueryParam(String)} 283 * first, then {@link #addQueryParam(String, Object)}, or call 284 * {@link #setQuery(Map)} which will set the entire query string. 285 * @return a map of String name/value pairs representing the URI's query 286 * string. 287 */ 288 public Map<String,Object> getQuery() { 289 Map<String,Object> params = new HashMap<String,Object>(); 290 List<NameValuePair> pairs = this.getQueryNVP(); 291 if ( pairs == null ) return null; 292 293 for ( NameValuePair pair : pairs ) { 294 295 String key = pair.getName(); 296 Object existing = params.get( key ); 297 298 if ( existing == null ) params.put( key, pair.getValue() ); 299 300 else if ( existing instanceof List<?> ) 301 ((List)existing).add( pair.getValue() ); 302 303 else { 304 List<String> vals = new ArrayList<String>(2); 305 vals.add( (String)existing ); 306 vals.add( pair.getValue() ); 307 params.put( key, vals ); 308 } 309 } 310 311 return params; 312 } 313 314 protected List<NameValuePair> getQueryNVP() { 315 if ( this.base.getQuery() == null ) return null; 316 List<NameValuePair> nvps = URLEncodedUtils.parse( this.base, ENC ); 317 List<NameValuePair> newList = new ArrayList<NameValuePair>(); 318 if ( nvps != null ) newList.addAll( nvps ); 319 return newList; 320 } 321 322 /** 323 * Indicates if the given parameter is already part of this URI's query 324 * string. 325 * @param name the query parameter name 326 * @return true if the given parameter name is found in the query string of 327 * the URI. 328 */ 329 public boolean hasQueryParam( String name ) { 330 return getQuery().get( name ) != null; 331 } 332 333 /** 334 * Remove the given query parameter from this URI's query string. 335 * @param param the query name to remove 336 * @return this URIBuilder instance, for method chaining. 337 * @throws URISyntaxException 338 */ 339 public URIBuilder removeQueryParam( String param ) throws URISyntaxException { 340 List<NameValuePair> params = getQueryNVP(); 341 NameValuePair found = null; 342 for ( NameValuePair nvp : params ) // BOO linear search. Assume the list is small. 343 if ( nvp.getName().equals( param ) ) { 344 found = nvp; 345 break; 346 } 347 348 if ( found == null ) throw new IllegalArgumentException( "Param '" + param + "' not found" ); 349 params.remove( found ); 350 this.setQueryNVP( params ); 351 return this; 352 } 353 354 protected URIBuilder addQueryParam( NameValuePair nvp ) throws URISyntaxException { 355 List<NameValuePair> params = getQueryNVP(); 356 if ( params == null ) params = new ArrayList<NameValuePair>(); 357 params.add( nvp ); 358 this.setQueryNVP( params ); 359 return this; 360 } 361 362 /** 363 * This will append a query parameter to the existing query string. If the given 364 * parameter is already part of the query string, it will be appended to. 365 * To replace the existing value of a certain parameter, either call 366 * {@link #removeQueryParam(String)} first, or use {@link #getQuery()}, 367 * modify the value in the map, then call {@link #setQuery(Map)}. 368 * @param param query parameter name 369 * @param value query parameter value (will be converted to a string if 370 * not null. If <code>value</code> is null, it will be set as the empty 371 * string. 372 * @return this URIBuilder instance, for method chaining. 373 * @throws URISyntaxException if the query parameter values cannot be 374 * converted to a valid URI. 375 * @see #setQuery(Map) 376 */ 377 public URIBuilder addQueryParam( String param, Object value ) throws URISyntaxException { 378 this.addQueryParam( new BasicNameValuePair( param, 379 ( value != null ) ? value.toString() : "" ) ); 380 return this; 381 } 382 383 protected URIBuilder addQueryParams( List<NameValuePair> nvp ) throws URISyntaxException { 384 List<NameValuePair> params = getQueryNVP(); 385 if ( params == null ) params = new ArrayList<NameValuePair>(); 386 params.addAll( nvp ); 387 this.setQueryNVP( params ); 388 return this; 389 } 390 391 /** 392 * Add these parameters to the URIBuilder's existing query string. 393 * Parameters may be passed either as a single map argument, or as a list 394 * of named arguments. e.g. 395 * <pre> uriBuilder.addQueryParams( [one:1,two:2] ) 396 * uriBuilder.addQueryParams( three : 3 ) </pre> 397 * 398 * If any of the parameters already exist in the URI query, these values 399 * will <strong>not</strong> replace them. Multiple values for the same 400 * query parameter may be added by putting them in a list. See 401 * {@link #setQuery(Map)}. 402 * 403 * @param params parameters to add to the existing URI query (if any). 404 * @return this URIBuilder instance, for method chaining. 405 * @throws URISyntaxException 406 */ 407 @SuppressWarnings("unchecked") 408 public URIBuilder addQueryParams( Map<?,?> params ) throws URISyntaxException { 409 List<NameValuePair> nvp = new ArrayList<NameValuePair>(); 410 for ( Object key : params.keySet() ) { 411 Object value = params.get( key ); 412 if ( value instanceof List ) { 413 for ( Object val : (List)value ) 414 nvp.add( new BasicNameValuePair( key.toString(), 415 ( val != null ) ? val.toString() : "" ) ); 416 } 417 else nvp.add( new BasicNameValuePair( key.toString(), 418 ( value != null ) ? value.toString() : "" ) ); 419 } 420 this.addQueryParams( nvp ); 421 return this; 422 } 423 424 /** 425 * The document fragment, without a preceeding '#'. Use <code>null</code> 426 * to use no document fragment. 427 * @param fragment 428 * @return this URIBuilder instance, for method chaining. 429 * @throws URISyntaxException if the given value contains illegal characters. 430 */ 431 public URIBuilder setFragment( String fragment ) throws URISyntaxException { 432 this.base = update( base.getScheme(), base.getUserInfo(), 433 base.getHost(), base.getPort(), base.getRawPath(), 434 base.getRawQuery(), new URI( null, null, null, fragment ).getRawFragment() ); 435 return this; 436 } 437 438 /** 439 * See {@link URI#getFragment()} 440 * @return the URI document fragment 441 */ 442 public String getFragment() { 443 return this.base.getFragment(); 444 } 445 446 /** 447 * Set the userInfo portion of the URI, or <code>null</code> if the URI 448 * should have no user information. 449 * @param userInfo 450 * @return this URIBuilder instance 451 * @throws URISyntaxException if the given value contains illegal characters. 452 */ 453 public URIBuilder setUserInfo( String userInfo ) throws URISyntaxException { 454 this.base = update( base.getScheme(), userInfo, 455 base.getHost(), base.getPort(), base.getRawPath(), 456 base.getRawQuery(), base.getRawFragment() ); 457 458 return this; 459 } 460 461 /** 462 * See {@link URI#getUserInfo()} 463 * @return the user info portion of the URI, or <code>null</code> if it 464 * is not specified. 465 */ 466 public String getUserInfo() { 467 return this.base.getUserInfo(); 468 } 469 470 /** 471 * Print this builder's URI representation. 472 */ 473 @Override public String toString() { 474 return base.toString(); 475 } 476 477 /** 478 * Convenience method to convert this object to a URL instance. 479 * @return this builder as a URL 480 * @throws MalformedURLException if the underlying URI does not represent a 481 * valid URL. 482 */ 483 public URL toURL() throws MalformedURLException { 484 return base.toURL(); 485 } 486 487 /** 488 * Convenience method to convert this object to a URI instance. 489 * @return this builder's underlying URI representation 490 */ 491 public URI toURI() { return this.base; } 492 493 /** 494 * Implementation of Groovy's <code>as</code> operator, to allow type 495 * conversion. 496 * @param type <code>URL</code>, <code>URL</code>, or <code>String</code>. 497 * @return a representation of this URIBuilder instance in the given type 498 * @throws MalformedURLException if <code>type</code> is URL and this 499 * URIBuilder instance does not represent a valid URL. 500 */ 501 public Object asType( Class<?> type ) throws MalformedURLException { 502 if ( type == URI.class ) return this.toURI(); 503 if ( type == URL.class ) return this.toURL(); 504 if ( type == String.class ) return this.toString(); 505 throw new ClassCastException( "Cannot cast instance of URIBuilder to class " + type ); 506 } 507 508 /** 509 * Create a copy of this URIBuilder instance. 510 */ 511 @Override 512 protected URIBuilder clone() { 513 return new URIBuilder( this.base ); 514 } 515 516 /** 517 * Determine if this URIBuilder is equal to another URIBuilder instance. 518 * @see URI#equals(Object) 519 * @return if <code>obj</code> is a URIBuilder instance whose underlying 520 * URI implementation is equal to this one's. 521 */ 522 @Override 523 public boolean equals( Object obj ) { 524 if ( ! ( obj instanceof URIBuilder) ) return false; 525 return this.base.equals( ((URIBuilder)obj).toURI() ); 526 } 527}