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    }