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