package org.jrecruiter.common;
//Copyright (c) 2009, Richard Kennard
//All rights reserved.
//
//Redistribution and use in source and binary forms, with or without
//modification, are permitted provided that the following conditions are met:
//* Redistributions of source code must retain the above copyright
//notice, this list of conditions and the following disclaimer.
//* Redistributions in binary form must reproduce the above copyright
//notice, this list of conditions and the following disclaimer in the
//documentation and/or other materials provided with the distribution.
//* Neither the name of Richard Kennard nor the
//names of its contributors may be used to endorse or promote products
//derived from this software without specific prior written permission.
//
//THIS SOFTWARE IS PROVIDED BY RICHARD KENNARD ''AS IS'' AND ANY
//EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//DISCLAIMED. IN NO EVENT SHALL RICHARD KENNARD BE LIABLE FOR ANY
//DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
/**
* Represents a www-form-urlencoded query string.
* <p>
* An instance of this class represents a query string encoded using the
* <code>www-form-urlencoded</code> encoding scheme, as defined by <a
* href="http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1">HTML 4.01 Specification:
* application/x-www-form-urlencoded</a>, and <a
* href="http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2">HTML 4.01
* Specification: Ampersands in URI attribute values</a>. This is a common encoding scheme of the
* query component of a URI, though the <a href="http://www.ietf.org/rfc/rfc2396.txt">RFC 2396 URI
* specification</a> itself does not define a specific format for the query component.
* <p>
* This class provides static methods for <a href="#create()">creating</a> UrlEncodedQueryString
* instances by <a href="#parse(java.lang.CharSequence)">parsing</a> URI and string forms. It can
* then be used to create, retrieve, update and delete parameters, and to re-apply the query string
* back to an existing URI.
* <p>
* <h4>Encoding and decoding</h4>
* UrlEncodedQueryString automatically encodes and decodes parameter names and values to and from
* <code>www-form-urlencoded</code> encoding by using <code>java.net.URLEncoder</code> and
* <code>java.net.URLDecoder</code>, which follow the <a
* href="http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars"> HTML 4.01 Specification:
* Non-ASCII characters in URI attribute values</a> recommendation.
* <h4>Multivalued parameters</h4>
* Often, parameter names are unique across the name/value pairs of a
* <code>www-form-urlencoded</code> query string. However, it is permitted for the same parameter
* name to appear in multiple name/value pairs, denoting that a single parameter has multiple
* values. This less common use case can lead to ambiguity when adding parameters - is the 'add' a
* 'replace' (of an existing parameter, if one with the same name already exists) or an 'append'
* (potentially creating a multivalued parameter, if one with the same name already exists)?
* <p>
* This requirement significantly shapes the <code>UrlEncodedQueryString</code> API. In particular
* there are:
* <ul>
* <li><code>set</code> methods for setting a parameter, potentially replacing an existing value
* <li><code>append</code> methods for adding a parameter, potentially creating a multivalued
* parameter
* <li><code>get</code> methods for returning a single value, even if the parameter has multiple
* values
* <li><code>getValues</code> methods for returning multiple values
* </ul>
* <h4>Retrieving parameters</h4>
* UrlEncodedQueryString can be used to parse and retrieve parameters from a query string by passing
* either a URI or a query string to its constructor:
* <p>
* <code>
* URI uri = new URI("http://java.sun.com?forum=2");<br/>
* UrlEncodedQueryString queryString = UrlEncodedQueryString.parse(uri);<br/>
* System.out.println(queryString.get("forum"));<br/>
* </code>
* <h4>Modifying parameters</h4>
* UrlEncodedQueryString can be used to set, append or remove parameters from a query string:
* <p>
* <code>
* URI uri = new URI("/forum/article.jsp?id=2¶=4");<br/>
* UrlEncodedQueryString queryString = UrlEncodedQueryString.parse(uri);<br/>
* queryString.set("id", 3);<br/>
* queryString.remove("para");<br/>
* System.out.println(queryString);<br/>
* </code>
* <p>
* When modifying parameters, the ordering of existing parameters is maintained. Parameters are
* <code>set</code> and <code>removed</code> in-place, while <code>appended</code> parameters
* are added to the end of the query string.
* <h4>Applying the Query</h4>
* UrlEncodedQueryString can be used to apply a modified query string back to a URI, creating a new
* URI:
* <p>
* <code>
* URI uri = new URI("/forum/article.jsp?id=2");<br/>
* UrlEncodedQueryString queryString = UrlEncodedQueryString.parse(uri);<br/>
* queryString.set("id", 3);<br/>
* uri = queryString.apply(uri);<br/>
* </code>
* <p>
* When reconstructing query strings, there are two valid separator parameters defined by the W3C
* (ampersand "&" and semicolon ";"), with ampersand being the most common. The
* <code>apply</code> and <code>toString</code> methods both default to using an ampersand, with
* overloaded forms for using a semicolon.
* <h4>Thread Safety</h4>
* This implementation is not synchronized. If multiple threads access a query string concurrently,
* and at least one of the threads modifies the query string, it must be synchronized externally.
* This is typically accomplished by synchronizing on some object that naturally encapsulates the
* query string.
*
* @author Richard Kennard
* @version 1.0
*/
public class UrlEncodedQueryString
{
//
// Public statics
//
/**
* Enumeration of recommended www-form-urlencoded separators.
* <p>
* Recommended separators are defined by <a
* href="http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1">HTML 4.01
* Specification: application/x-www-form-urlencoded</a> and <a
* href="http://www.w3.org/TR/html401/appendix/notes.html#h-B.2.2">HTML 4.01 Specification:
* Ampersands in URI attribute values</a>.
* <p>
* <em>All</em> separators are recognised when parsing query strings. <em>One</em> separator
* may be passed to <code>toString</code> and <code>apply</code> when outputting query
* strings.
*/
public static enum Separator
{
/**
* An ampersand <code>&</code> - the separator recommended by <a
* href="http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1">HTML 4.01
* Specification: application/x-www-form-urlencoded</a>.
*/
AMPERSAND
{
/**
* Returns a String representation of this Separator.
* <p>
* The String representation matches that defined by the <a
* href="http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1">HTML 4.01
* Specification: application/x-www-form-urlencoded</a>.
*/
@Override
public String toString()
{
return "&";
}
},
/**
* A semicolon <code>;</code> - the separator recommended by <a
* href="http://www.w3.org/TR/html401/appendix/notes.html#h-B.2.2">HTML 4.01 Specification:
* Ampersands in URI attribute values</a>.
*/
SEMICOLON
{
/**
* Returns a String representation of this Separator.
* <p>
* The String representation matches that defined by the <a
* href="http://www.w3.org/TR/html401/appendix/notes.html#h-B.2.2">HTML 4.01
* Specification: Ampersands in URI attribute values</a>.
*/
@Override
public String toString()
{
return ";";
}
};
}
/**
* Creates an empty UrlEncodedQueryString.
* <p>
* Calling <code>toString()</code> on the created instance will return an empty String.
*/
public static UrlEncodedQueryString create()
{
return new UrlEncodedQueryString();
}
/**
* Creates a UrlEncodedQueryString from the given Map.
* <p>
* The order the parameters are created in corresponds to the iteration order of the Map.
*
* @param parameterMap
* <code>Map</code> containing parameter names and values.
*/
public static UrlEncodedQueryString create( Map<String, List<String>> parameterMap )
{
UrlEncodedQueryString queryString = new UrlEncodedQueryString();
// Defensively copy the List<String>'s
for ( Map.Entry<String, List<String>> entry : parameterMap.entrySet() )
{
queryString.queryMap.put( entry.getKey(), new ArrayList<String>( entry.getValue() ) );
}
return queryString;
}
/**
* Creates a UrlEncodedQueryString by parsing the given query string.
* <p>
* This method assumes the given string is the <code>www-form-urlencoded</code> query
* component of a URI. When parsing, all <a
* href="UrlEncodedQueryString.Separator.html">Separators</a> are recognised.
* <p>
* The result of calling this method with a string that is not <code>www-form-urlencoded</code>
* (eg. passing an entire URI, not just its query string) will likely be mismatched parameter
* names.
*
* @param query
* query string to be parsed
*/
public static UrlEncodedQueryString parse( final CharSequence query )
{
UrlEncodedQueryString queryString = new UrlEncodedQueryString();
// Note: import to call appendOrSet with 'true', in
// case the given query contains multi-valued parameters
queryString.appendOrSet( query, true );
return queryString;
}
/**
* Creates a UrlEncodedQueryString by extracting and parsing the query component from the given
* URI.
* <p>
* This method assumes the query component is <code>www-form-urlencoded</code>. When parsing,
* all separators from the Separators enum are recognised.
* <p>
* The result of calling this method with a query component that is not
* <code>www-form-urlencoded</code> will likely be mismatched parameter names.
*
* @param uri
* URI to be parsed
*/
public static UrlEncodedQueryString parse( final URI uri )
{
// Note: use uri.getRawQuery, not uri.getQuery, in case the
// query parameters contain encoded ampersands (%26)
return parse( uri.getRawQuery() );
}
//
// Private statics
//
/**
* Separators to honour when parsing query strings.
* <p>
* <em>All</em> Separators are recognized when parsing parameters, regardless of what the user
* later nominates as their <code>toString</code> output parameter.
*/
private final static String PARSE_PARAMETER_SEPARATORS = String.valueOf( Separator.AMPERSAND ) + Separator.SEMICOLON;
//
// Private members
//
/**
* Map of query parameters.
*/
// Note: we initialize this Map upon object creation because, realistically, it
// is always going to be needed (eg. there is little point lazy-initializing it)
private final Map<String, List<String>> queryMap = new LinkedHashMap<String, List<String>>();
//
// Public methods
//
/**
* Returns the value of the named parameter as a String, or <code>null</code> if the parameter
* does not exist.
* <p>
* You should only use this method when you are sure the parameter has only one value. If the
* parameter might have more than one value, use <a
* href="#getValues(java.lang.String)">getValues</a>.
* <p>
* If you use this method with a multivalued parameter, the value returned is equal to the first
* value in the List returned by <a href="#getValues(java.lang.String)">getValues</a>.
*
* @param name
* <code>String</code> specifying the name of the parameter
* @return <code>String</code> representing the single value of the parameter, or
* <code>null</code> if the parameter does not exist
*/
public String get( final String name )
{
List<String> parameters = getValues( name );
if ( parameters == null || parameters.isEmpty() ) {
return null;
}
return parameters.get( 0 );
}
/**
* Returns an <code>Iterator</code> of <code>String</code> objects containing the names of
* the parameters. If there are no parameters, the method returns an empty Iterator. For names
* with multiple values, only one copy of the name is returned.
*
* @return an <code>Iterator</code> of <code>String</code> objects, each String containing
* the name of a parameter; or an empty Iterator if there are no parameters
*/
public Iterator<String> getNames()
{
return this.queryMap.keySet().iterator();
}
/**
* Returns a List of <code>String</code> objects containing all of the values the named
* parameter has, or <code>null</code> if the parameter does not exist.
* <p>
* If the parameter has a single value, the List has a size of 1.
*
* @param name
* name of the parameter to retrieve
* @return a List of String objects containing the parameter's values, or <code>null</code> if
* the paramater does not exist
*/
public List<String> getValues( final String name )
{
return this.queryMap.get( name );
}
/**
* Returns a mutable <code>Map</code> of the query parameters.
*
* @return <code>Map</code> containing parameter names as keys and parameter values as map
* values. The keys in the parameter map are of type <code>String</code>. The values
* in the parameter map are Lists of type <code>String</code>, and their ordering is
* consistent with their ordering in the query string. Will never return
* <code>null</code>.
*/
public Map<String, List<String>> getMap()
{
LinkedHashMap<String, List<String>> map = new LinkedHashMap<String, List<String>>();
// Defensively copy the List<String>'s
for ( Map.Entry<String, List<String>> entry : this.queryMap.entrySet() )
{
List<String> listValues = entry.getValue();
map.put( entry.getKey(), new ArrayList<String>( listValues ) );
}
return map;
}
/**
* Sets a query parameter.
* <p>
* If one or more parameters with this name already exist, they will be replaced with a single
* parameter with the given value. If no such parameters exist, one will be added.
*
* @param name
* name of the query parameter
* @param value
* value of the query parameter. If <code>null</code>, the parameter is removed
* @return a reference to this object
*/
public UrlEncodedQueryString set( final String name, final String value )
{
appendOrSet( name, value, false );
return this;
}
/**
* Sets a query parameter.
* <p>
* If one or more parameters with this name already exist, they will be replaced with a single
* parameter with the given value. If no such parameters exist, one will be added.
* <p>
* This version of <code>set</code> accepts a <code>Number</code> suitable for auto-boxing.
* For example:
* <p>
* <code>
* queryString.set( "id", 3 );<br/>
* </code>
*
* @param name
* name of the query parameter
* @param value
* value of the query parameter. If <code>null</code>, the parameter is removed
* @return a reference to this object
*/
public UrlEncodedQueryString set( final String name, final Number value )
{
if ( value == null )
{
remove( name );
return this;
}
appendOrSet( name, value.toString(), false );
return this;
}
/**
* Sets query parameters from a <code>www-form-urlencoded</code> string.
* <p>
* The given string is assumed to be in <code>www-form-urlencoded</code> format. The result of
* passing a string not in <code>www-form-urlencoded</code> format (eg. passing an entire URI,
* not just its query string) will likely be mismatched parameter names.
* <p>
* The given string is parsed into named parameters, and each is added to the existing
* parameters. If a parameter with the same name already exists, it is replaced with a single
* parameter with the given value. If the same parameter name appears more than once in the
* given string, it is stored as a multivalued parameter. When parsing, all <a
* href="UrlEncodedQueryString.Separator.html">Separators</a> are recognised.
*
* @param query
* <code>www-form-urlencoded</code> string. If <code>null</code>, does nothing
* @return a reference to this object
*/
public UrlEncodedQueryString set( final String query )
{
appendOrSet( query, false );
return this;
}
/**
* Appends a query parameter.
* <p>
* If one or more parameters with this name already exist, their value will be preserved and the
* given value will be stored as a multivalued parameter. If no such parameters exist, one will
* be added.
*
* @param name
* name of the query parameter
* @param value
* value of the query parameter. If <code>null</code>, does nothing
* @return a reference to this object
*/
public UrlEncodedQueryString append( final String name, final String value )
{
appendOrSet( name, value, true );
return this;
}
/**
* Appends a query parameter.
* <p>
* If one or more parameters with this name already exist, their value will be preserved and the
* given value will be stored as a multivalued parameter. If no such parameters exist, one will
* be added.
* <p>
* This version of <code>append</code> accepts a <code>Number</code> suitable for
* auto-boxing. For example:
* <p>
* <code>
* queryString.append( "id", 3 );<br/>
* </code>
*
* @param name
* name of the query parameter
* @param value
* value of the query parameter. If <code>null</code>, does nothing
* @return a reference to this object
*/
public UrlEncodedQueryString append( final String name, final Number value )
{
appendOrSet( name, value.toString(), true );
return this;
}
/**
* Appends query parameters from a <code>www-form-urlencoded</code> string.
* <p>
* The given string is assumed to be in <code>www-form-urlencoded</code> format. The result of
* passing a string not in <code>www-form-urlencoded</code> format (eg. passing an entire URI,
* not just its query string) will likely be mismatched parameter names.
* <p>
* The given string is parsed into named parameters, and appended to the existing parameters. If
* a parameter with the same name already exists, or if the same parameter name appears more
* than once in the given string, it is stored as a multivalued parameter. When parsing, all <a
* href="UrlEncodedQueryString.Separator.html">Separators</a> are recognised.
*
* @param query
* <code>www-form-urlencoded</code> string. If <code>null</code>, does nothing
* @return a reference to this object
*/
public UrlEncodedQueryString append( final String query )
{
appendOrSet( query, true );
return this;
}
/**
* Returns whether the query string is empty.
*
* @return true if the query string has no parameters
*/
public boolean isEmpty()
{
return queryMap.isEmpty();
}
/**
* Removes the named query parameter.
* <p>
* If the parameter has multiple values, all its values are removed.
*
* @param name
* name of the parameter to remove
* @return a reference to this object
*/
public UrlEncodedQueryString remove( final String name )
{
appendOrSet( name, null, false );
return this;
}
/**
* Applies the query string to the given URI.
* <p>
* A copy of the given URI is taken and its existing query string, if there is one, is replaced.
* The query string parameters are separated by <code>Separator.Ampersand</code>.
*
* @param uri
* URI to copy and update
* @return a copy of the given URI, with an updated query string
*/
public URI apply( URI uri )
{
return apply( uri, Separator.AMPERSAND );
}
/**
* Applies the query string to the given URI, using the given separator between parameters.
* <p>
* A copy of the given URI is taken and its existing query string, if there is one, is replaced.
* The query string parameters are separated using the given <code>Separator</code>.
*
* @param uri
* URI to copy and update
* @param separator
* separator to use between parameters
* @return a copy of the given URI, with an updated query string
*/
public URI apply( URI uri, Separator separator )
{
// Note this code is essentially a copy of 'java.net.URI.defineString',
// which is private. We cannot use the 'new URI( scheme, userInfo, ... )' or
// 'new URI( scheme, authority, ... )' constructors because they double
// encode the query string using 'java.net.URI.quote'
StringBuilder builder = new StringBuilder();
if ( uri.getScheme() != null )
{
builder.append( uri.getScheme() );
builder.append( ':' );
}
if ( uri.getHost() != null )
{
builder.append( "//" );
if ( uri.getUserInfo() != null )
{
builder.append( uri.getUserInfo() );
builder.append( '@' );
}
builder.append( uri.getHost() );
if ( uri.getPort() != -1 )
{
builder.append( ':' );
builder.append( uri.getPort() );
}
}
else if ( uri.getAuthority() != null )
{
builder.append( "//" );
builder.append( uri.getAuthority() );
}
if ( uri.getPath() != null ) {
builder.append( uri.getPath() );
}
String query = toString( separator );
if ( query.length() != 0 )
{
builder.append( '?' );
builder.append( query );
}
if ( uri.getFragment() != null )
{
builder.append( '#' );
builder.append( uri.getFragment() );
}
try
{
return new URI( builder.toString() );
}
catch ( URISyntaxException e )
{
// Can never happen, as the given URI will always be valid,
// and getQuery() will always return a valid query string
throw new RuntimeException( e );
}
}
/**
* Compares the specified object with this UrlEncodedQueryString for equality.
* <p>
* Returns <code>true</code> if the given object is also a UrlEncodedQueryString and the two
* UrlEncodedQueryStrings have the same parameters. More formally, two UrlEncodedQueryStrings
* <code>t1</code> and <code>t2</code> represent the same UrlEncodedQueryString if
* <code>t1.toString().equals(t2.toString())</code>. This ensures that the
* <code>equals</code> method checks the ordering, as well as the existence, of every
* parameter.
* <p>
* Clients interested only in the existence, not the ordering, of parameters are recommended to
* use <code>getMap().equals</code>.
* <p>
* This implementation first checks if the specified object is this UrlEncodedQueryString; if so
* it returns <code>true</code>. Then, it checks if the specified object is a
* UrlEncodedQueryString whose toString() is identical to the toString() of this
* UrlEncodedQueryString; if not, it returns <code>false</code>. Otherwise, it returns
* <code>true</code>
*
* @param obj
* object to be compared for equality with this UrlEncodedQueryString.
* @return <code>true</code> if the specified object is equal to this UrlEncodedQueryString.
*/
@Override
public boolean equals( Object obj )
{
if ( obj == this ) {
return true;
}
if ( !( obj instanceof UrlEncodedQueryString ) ) {
return false;
}
String query = toString();
String thatQuery = ( (UrlEncodedQueryString) obj ).toString();
return query.equals( thatQuery );
}
/**
* Returns a hash code value for the UrlEncodedQueryString.
* <p>
* The hash code of the UrlEncodedQueryString is defined to be the hash code of the
* <code>String</code> returned by toString(). This ensures the ordering, as well as the
* existence, of parameters is taken into account.
* <p>
* Clients interested only in the existence, not the ordering, of parameters are recommended to
* use <code>getMap().hashCode</code>.
*
* @return a hash code value for this UrlEncodedQueryString.
*/
@Override
public int hashCode()
{
return toString().hashCode();
}
/**
* Returns a <code>www-form-urlencoded</code> string of the query parameters.
* <p>
* The HTML specification recommends two parameter separators in <a
* href="http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1">HTML 4.01
* Specification: application/x-www-form-urlencoded</a> and <a
* href="http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2">HTML 4.01
* Specification: Ampersands in URI attribute values</a>. Of those, the ampersand is the more
* commonly used and this method defaults to that.
*
* @return <code>www-form-urlencoded</code> string, or <code>null</code> if there are no
* parameters.
*/
@Override
public String toString()
{
return toString( Separator.AMPERSAND );
}
/**
* Returns a <code>www-form-urlencoded</code> string of the query parameters, using the given
* separator between parameters.
*
* @param separator
* separator to use between parameters
* @return <code>www-form-urlencoded</code> string, or an empty String if there are no
* parameters
*/
// Note: this method takes a Separator, not just any String. Taking any String may
// be useful in some circumstances (eg. you could pass '&' to generate query
// strings for use in HTML pages) but would break the implied contract between
// toString() and parse() (eg. you can always parse() what you toString() ).
//
// It was thought better to leave it to the user to explictly break this contract
// (eg. toString().replaceAll( '&', '&' ))
public String toString( Separator separator )
{
StringBuilder builder = new StringBuilder();
for ( String strName : this.queryMap.keySet() )
{
for ( String strValue : this.queryMap.get( strName ) )
{
if ( builder.length() != 0 ) {
builder.append( separator );
}
// Encode names and values. Do this in toString(), rather than
// append/set, so that the Map always contains the
// raw, unencoded values
try
{
builder.append( URLEncoder.encode( strName, "UTF-8" ) );
if ( !"".equals( strValue ) )
{
builder.append( '=' );
builder.append( URLEncoder.encode( strValue, "UTF-8" ) );
}
}
catch ( UnsupportedEncodingException e )
{
// Should never happen. UTF-8 should always be available
// according to Java spec
throw new RuntimeException( e );
}
}
}
return builder.toString();
}
//
// Private methods
//
/**
* Private constructor.
* <p>
* Clients should use one of the <code>create</code> or <code>parse</code> methods to create
* a <code>UrlEncodedQueryString</code>.
*/
private UrlEncodedQueryString()
{
// Can never be called
}
/**
* Helper method for append and set
*
* @param name
* the parameter's name
* @param value
* the parameter's value
* @param append
* whether to append (or set)
*/
private void appendOrSet( final String name, final String value, final boolean append )
{
if ( name == null ) {
throw new NullPointerException( "name" );
}
if ( value == null && append ) {
return;
}
// If we're appending, and there's an existing parameter...
if ( append )
{
List<String> listValues = this.queryMap.get( name );
// ...add to it
if ( listValues != null )
{
listValues.add( value );
return;
}
}
// ...otherwise, if we're setting and the value is null...
else if ( value == null )
{
// ...remove it
this.queryMap.remove( name );
return;
}
// ...otherwise, create a new one
List<String> listValues = new ArrayList<String>();
listValues.add( value );
this.queryMap.put( name, listValues );
}
/**
* Helper method for append and set
*
* @param query
* <code>www-form-urlencoded</code> string
* @param append
* whether to append (or set)
*/
private void appendOrSet( final CharSequence parameters, final boolean append )
{
// Nothing to do?
if ( parameters == null ) {
return;
}
// Note we always parse using PARSE_PARAMETER_SEPARATORS, regardless
// of what the user later nominates as their output parameter
// separator using toString()
StringTokenizer tokenizer = new StringTokenizer( parameters.toString(), PARSE_PARAMETER_SEPARATORS );
Set<String> setAlreadyParsed = null;
while ( tokenizer.hasMoreTokens() )
{
String parameter = tokenizer.nextToken();
int indexOf = parameter.indexOf( '=' );
String strName;
String strValue;
try
{
if ( indexOf == -1 )
{
strName = parameter;
strValue = "";
}
else
{
strName = parameter.substring( 0, indexOf );
strValue = parameter.substring( indexOf + 1 );
}
// When not appending, the first time we see a given
// name it is important to remove it from the existing
// parameters
if ( !append )
{
if ( setAlreadyParsed == null ) {
setAlreadyParsed = new HashSet<String>();
}
if ( !setAlreadyParsed.contains( strName ) ) {
remove( strName );
}
setAlreadyParsed.add( strName );
}
appendOrSet( strName, URLDecoder.decode( strValue, "UTF-8" ), true );
}
catch ( UnsupportedEncodingException e )
{
// Should never happen. UTF-8 should always be available
// according to Java spec
throw new RuntimeException( e );
}
}
}
}