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}