/*
* Copyright 2008-2011 Thomas Nichols. http://blog.thomnichols.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You are receiving this code free of charge, which represents many hours of
* effort from other individuals and corporations. As a responsible member
* of the community, you are encouraged (but not required) to donate any
* enhancements or improvements back to the community under a similar open
* source license. Thank you. -TMN
*/
package groovyx.net.http;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
/**
* This class implements a mutable URI. All <code>set</code>, <code>add</code>
* and <code>remove</code> methods affect this class' internal URI
* representation. All mutator methods support chaining, e.g.
* <pre>
* new URIBuilder("http://www.google.com/")
* .setScheme( "https" )
* .setPort( 443 )
* .setPath( "some/path" )
* .toString();
* </pre>
* A slightly more 'Groovy' version would be:
* <pre>
* new URIBuilder('http://www.google.com/').with {
* scheme = 'https'
* port = 443
* path = 'some/path'
* query = [p1:1, p2:'two']
* return it
* }.toString()
* </pre>
* @author <a href='mailto:tomstrummer+httpbuilder@gmail.com'>Tom Nichols</a>
*/
public class URIBuilder implements Cloneable {
protected URI base;
private final String ENC = "UTF-8";
public URIBuilder( String url ) throws URISyntaxException {
base = new URI(url);
}
public URIBuilder( URL url ) throws URISyntaxException {
this.base = url.toURI();
}
/**
* @throws IllegalArgumentException if uri is null
* @param uri
*/
public URIBuilder( URI uri ) throws IllegalArgumentException {
if ( uri == null )
throw new IllegalArgumentException( "uri cannot be null" );
this.base = uri;
}
/**
* Utility method to convert a number of type to a URI instance.
* @param uri a {@link URI}, {@link URL} or any object that produces a
* valid URI string from its <code>toString()</code> result.
* @return a valid URI parsed from the given object
* @throws URISyntaxException
*/
public static URI convertToURI( Object uri ) throws URISyntaxException {
if ( uri instanceof URI ) return (URI)uri;
if ( uri instanceof URL ) return ((URL)uri).toURI();
if ( uri instanceof URIBuilder ) return ((URIBuilder)uri).toURI();
return new URI( uri.toString() ); // assume any other object type produces a valid URI string
}
protected URI update( String scheme, String userInfo, String host, int port,
String path, String query, String fragment ) throws URISyntaxException {
URI u = new URI( scheme, userInfo, host, port, base.getPath(), null, null );
StringBuilder sb = new StringBuilder();
if ( path != null ) sb.append( path );
if ( query != null )
sb.append( '?' ).append( query );
if ( fragment != null ) sb.append( '#' ).append( fragment );
return u.resolve( sb.toString() );
}
/**
* Set the URI scheme, AKA the 'protocol.' e.g.
* <code>setScheme('https')</code>
* @throws URISyntaxException if the given scheme contains illegal characters.
*/
public URIBuilder setScheme( String scheme ) throws URISyntaxException {
this.base = update( scheme, base.getUserInfo(),
base.getHost(), base.getPort(),
base.getRawPath(), base.getRawQuery(), base.getRawFragment() );
return this;
}
/**
* Get the scheme for this URI. See {@link URI#getScheme()}
* @return the scheme portion of the URI
*/
public String getScheme() {
return this.base.getScheme();
}
/**
* Set the port for this URI, or <code>-1</code> to unset the port.
* @param port
* @return this URIBuilder instance
* @throws URISyntaxException
*/
public URIBuilder setPort( int port ) throws URISyntaxException {
this.base = update( base.getScheme(), base.getUserInfo(),
base.getHost(), port, base.getRawPath(),
base.getRawQuery(), base.getRawFragment() );
return this;
}
/**
* See {@link URI#getPort()}
* @return the port portion of this URI (-1 if a port is not specified.)
*/
public int getPort() {
return this.base.getPort();
}
/**
* Set the host portion of this URI.
* @param host
* @return this URIBuilder instance
* @throws URISyntaxException if the host parameter contains illegal characters.
*/
public URIBuilder setHost( String host ) throws URISyntaxException {
this.base = update( base.getScheme(), base.getUserInfo(),
host, base.getPort(), base.getRawPath(),
base.getRawQuery(), base.getRawFragment() );
return this;
}
/**
* See {@link URI#getHost()}
* @return the host portion of the URI
*/
public String getHost() {
return base.getHost();
}
/**
* Set the path component of this URI. The value may be absolute or
* relative to the current path.
* e.g. <pre>
* def uri = new URIBuilder( 'http://localhost/p1/p2?a=1' )
*
* uri.path = '/p3/p2'
* assert uri.toString() == 'http://localhost/p3/p2?a=1'
*
* uri.path = 'p2a'
* assert uri.toString() == 'http://localhost/p3/p2a?a=1'
*
* uri.path = '../p4'
* assert uri.toString() == 'http://localhost/p4?a=1&b=2&c=3#frag'
* <pre>
* @param path the path portion of this URI, relative to the current URI.
* @return this URIBuilder instance, for method chaining.
* @throws URISyntaxException if the given path contains characters that
* cannot be converted to a valid URI
*/
public URIBuilder setPath( String path ) throws URISyntaxException {
this.base = update( base.getScheme(), base.getUserInfo(),
base.getHost(), base.getPort(),
new URI( null, null, path, null, null ).getRawPath(),
base.getRawQuery(), base.getRawFragment() );
return this;
}
/**
* Note that this property is <strong>not</strong> necessarily reflexive
* with the {@link #setPath(String)} method! <code>URIBuilder.setPath()</code>
* will resolve a relative path, whereas this method will always return the
* full, absolute path.
* See {@link URI#getPath()}
* @return the full path portion of the URI.
*/
public String getPath() {
return this.base.getPath();
}
/* TODO null/ zero-size check if this is ever made public */
protected URIBuilder setQueryNVP( List<NameValuePair> nvp ) throws URISyntaxException {
/* Passing the query string in the URI constructor will
* double-escape query parameters and goober things up. So we have
* to create a full path+query+fragment and use URI#resolve() to
* create the new URI. */
StringBuilder sb = new StringBuilder();
String path = base.getRawPath();
if ( path != null ) sb.append( path );
sb.append( '?' );
sb.append( URLEncodedUtils.format( nvp, ENC ) );
String frag = base.getRawFragment();
if ( frag != null ) sb.append( '#' ).append( frag );
this.base = base.resolve( sb.toString() );
return this;
}
/**
* Set the query portion of the URI. For query parameters with multiple
* values, put the values in a list like so:
* <pre>uri.query = [ p1:'val1', p2:['val2', 'val3'] ]
* // will produce a query string of ?p1=val1&p2=val2&p2=val3</pre>
*
* @param params a Map of parameters that will be transformed into the query string
* @return this URIBuilder instance, for method chaining.
* @throws URISyntaxException
*/
public URIBuilder setQuery( Map<?,?> params ) throws URISyntaxException {
if ( params == null || params.size() < 1 ) {
this.base = new URI( base.getScheme(), base.getUserInfo(),
base.getHost(), base.getPort(), base.getPath(),
null, base.getFragment() );
}
else {
List<NameValuePair> nvp = new ArrayList<NameValuePair>(params.size());
for ( Object key : params.keySet() ) {
Object value = params.get(key);
if ( value instanceof List<?> ) {
for (Object val : (List<?>)value )
nvp.add( new BasicNameValuePair( key.toString(),
( val != null ) ? val.toString() : "" ) );
}
else nvp.add( new BasicNameValuePair( key.toString(),
( value != null ) ? value.toString() : "" ) );
}
this.setQueryNVP( nvp );
}
return this;
}
/**
* Set the raw, already-escaped query string. No additional escaping will
* be done on the string.
* @param query
* @return
*/
public URIBuilder setRawQuery( String query ) throws URISyntaxException {
this.base = update( base.getScheme(), base.getUserInfo(),
base.getHost(), base.getPort(),
base.getRawPath(), query, base.getRawFragment() );
return this;
}
/**
* Get the query string as a map for convenience. If any parameter contains
* multiple values (e.g. <code>p1=one&p1=two</code>) both values will be
* inserted into a list for that paramter key (<code>[p1 : ['one','two']]
* </code>). Note that this is not a "live" map. Therefore, you cannot
* call
* <pre> uri.query.a = 'BCD'</pre>
* You will not modify the query string but instead the generated map of
* parameters. Instead, you need to use {@link #removeQueryParam(String)}
* first, then {@link #addQueryParam(String, Object)}, or call
* {@link #setQuery(Map)} which will set the entire query string.
* @return a map of String name/value pairs representing the URI's query
* string.
*/
public Map<String,Object> getQuery() {
Map<String,Object> params = new HashMap<String,Object>();
List<NameValuePair> pairs = this.getQueryNVP();
if ( pairs == null ) return null;
for ( NameValuePair pair : pairs ) {
String key = pair.getName();
Object existing = params.get( key );
if ( existing == null ) params.put( key, pair.getValue() );
else if ( existing instanceof List<?> )
((List)existing).add( pair.getValue() );
else {
List<String> vals = new ArrayList<String>(2);
vals.add( (String)existing );
vals.add( pair.getValue() );
params.put( key, vals );
}
}
return params;
}
protected List<NameValuePair> getQueryNVP() {
if ( this.base.getQuery() == null ) return null;
List<NameValuePair> nvps = URLEncodedUtils.parse( this.base, ENC );
List<NameValuePair> newList = new ArrayList<NameValuePair>();
if ( nvps != null ) newList.addAll( nvps );
return newList;
}
/**
* Indicates if the given parameter is already part of this URI's query
* string.
* @param name the query parameter name
* @return true if the given parameter name is found in the query string of
* the URI.
*/
public boolean hasQueryParam( String name ) {
return getQuery().get( name ) != null;
}
/**
* Remove the given query parameter from this URI's query string.
* @param param the query name to remove
* @return this URIBuilder instance, for method chaining.
* @throws URISyntaxException
*/
public URIBuilder removeQueryParam( String param ) throws URISyntaxException {
List<NameValuePair> params = getQueryNVP();
NameValuePair found = null;
for ( NameValuePair nvp : params ) // BOO linear search. Assume the list is small.
if ( nvp.getName().equals( param ) ) {
found = nvp;
break;
}
if ( found == null ) throw new IllegalArgumentException( "Param '" + param + "' not found" );
params.remove( found );
this.setQueryNVP( params );
return this;
}
protected URIBuilder addQueryParam( NameValuePair nvp ) throws URISyntaxException {
List<NameValuePair> params = getQueryNVP();
if ( params == null ) params = new ArrayList<NameValuePair>();
params.add( nvp );
this.setQueryNVP( params );
return this;
}
/**
* This will append a query parameter to the existing query string. If the given
* parameter is already part of the query string, it will be appended to.
* To replace the existing value of a certain parameter, either call
* {@link #removeQueryParam(String)} first, or use {@link #getQuery()},
* modify the value in the map, then call {@link #setQuery(Map)}.
* @param param query parameter name
* @param value query parameter value (will be converted to a string if
* not null. If <code>value</code> is null, it will be set as the empty
* string.
* @return this URIBuilder instance, for method chaining.
* @throws URISyntaxException if the query parameter values cannot be
* converted to a valid URI.
* @see #setQuery(Map)
*/
public URIBuilder addQueryParam( String param, Object value ) throws URISyntaxException {
this.addQueryParam( new BasicNameValuePair( param,
( value != null ) ? value.toString() : "" ) );
return this;
}
protected URIBuilder addQueryParams( List<NameValuePair> nvp ) throws URISyntaxException {
List<NameValuePair> params = getQueryNVP();
if ( params == null ) params = new ArrayList<NameValuePair>();
params.addAll( nvp );
this.setQueryNVP( params );
return this;
}
/**
* Add these parameters to the URIBuilder's existing query string.
* Parameters may be passed either as a single map argument, or as a list
* of named arguments. e.g.
* <pre> uriBuilder.addQueryParams( [one:1,two:2] )
* uriBuilder.addQueryParams( three : 3 ) </pre>
*
* If any of the parameters already exist in the URI query, these values
* will <strong>not</strong> replace them. Multiple values for the same
* query parameter may be added by putting them in a list. See
* {@link #setQuery(Map)}.
*
* @param params parameters to add to the existing URI query (if any).
* @return this URIBuilder instance, for method chaining.
* @throws URISyntaxException
*/
@SuppressWarnings("unchecked")
public URIBuilder addQueryParams( Map<?,?> params ) throws URISyntaxException {
List<NameValuePair> nvp = new ArrayList<NameValuePair>();
for ( Object key : params.keySet() ) {
Object value = params.get( key );
if ( value instanceof List ) {
for ( Object val : (List)value )
nvp.add( new BasicNameValuePair( key.toString(),
( val != null ) ? val.toString() : "" ) );
}
else nvp.add( new BasicNameValuePair( key.toString(),
( value != null ) ? value.toString() : "" ) );
}
this.addQueryParams( nvp );
return this;
}
/**
* The document fragment, without a preceeding '#'. Use <code>null</code>
* to use no document fragment.
* @param fragment
* @return this URIBuilder instance, for method chaining.
* @throws URISyntaxException if the given value contains illegal characters.
*/
public URIBuilder setFragment( String fragment ) throws URISyntaxException {
this.base = update( base.getScheme(), base.getUserInfo(),
base.getHost(), base.getPort(), base.getRawPath(),
base.getRawQuery(), new URI( null, null, null, fragment ).getRawFragment() );
return this;
}
/**
* See {@link URI#getFragment()}
* @return the URI document fragment
*/
public String getFragment() {
return this.base.getFragment();
}
/**
* Set the userInfo portion of the URI, or <code>null</code> if the URI
* should have no user information.
* @param userInfo
* @return this URIBuilder instance
* @throws URISyntaxException if the given value contains illegal characters.
*/
public URIBuilder setUserInfo( String userInfo ) throws URISyntaxException {
this.base = update( base.getScheme(), userInfo,
base.getHost(), base.getPort(), base.getRawPath(),
base.getRawQuery(), base.getRawFragment() );
return this;
}
/**
* See {@link URI#getUserInfo()}
* @return the user info portion of the URI, or <code>null</code> if it
* is not specified.
*/
public String getUserInfo() {
return this.base.getUserInfo();
}
/**
* Print this builder's URI representation.
*/
@Override public String toString() {
return base.toString();
}
/**
* Convenience method to convert this object to a URL instance.
* @return this builder as a URL
* @throws MalformedURLException if the underlying URI does not represent a
* valid URL.
*/
public URL toURL() throws MalformedURLException {
return base.toURL();
}
/**
* Convenience method to convert this object to a URI instance.
* @return this builder's underlying URI representation
*/
public URI toURI() { return this.base; }
/**
* Implementation of Groovy's <code>as</code> operator, to allow type
* conversion.
* @param type <code>URL</code>, <code>URL</code>, or <code>String</code>.
* @return a representation of this URIBuilder instance in the given type
* @throws MalformedURLException if <code>type</code> is URL and this
* URIBuilder instance does not represent a valid URL.
*/
public Object asType( Class<?> type ) throws MalformedURLException {
if ( type == URI.class ) return this.toURI();
if ( type == URL.class ) return this.toURL();
if ( type == String.class ) return this.toString();
throw new ClassCastException( "Cannot cast instance of URIBuilder to class " + type );
}
/**
* Create a copy of this URIBuilder instance.
*/
@Override
protected URIBuilder clone() {
return new URIBuilder( this.base );
}
/**
* Determine if this URIBuilder is equal to another URIBuilder instance.
* @see URI#equals(Object)
* @return if <code>obj</code> is a URIBuilder instance whose underlying
* URI implementation is equal to this one's.
*/
@Override
public boolean equals( Object obj ) {
if ( ! ( obj instanceof URIBuilder) ) return false;
return this.base.equals( ((URIBuilder)obj).toURI() );
}
}