Steve,
Fair enough.
I downloaded NetBeans, cloned the OpenJDK repo, and updated my code. I tried to
submit a patch but http://openjdk.java.net/contribute says to use
https://bugs.openjdk.java.net, which says...
"this site will only be accepting and tracking patch contributions to the
OpenJDK 6 and 7 forests"
I was hoping you could help from here? I attach the updated code. As I said, it
was originally approved by 5 out of 6 CCC members. The only outstanding
issue was whether it overlapped with JSR 3011 (which at the time was just
getting started). I think this is a moot point now.
Regards,
Richard.
On 17/10/2011 10:22 PM, Steve Poole wrote:
> On Sun, 2011-10-16 at 20:41 -0700, kennardconsulting wrote:
>> Hi guys,
>>
>> So I'm just back from JavaOne 2011 and I'm all fired up about Open JDK again
>> :)
>>
>> I thought it may be worth revisiting RFE 6306820? All the hard work for this
>> RFE has already been done. There is a solid implementation, approved by 5
>> out of 6 CCC members, and in real-world use for over 4 years:
>>
>> http://java.net/projects/urlencodedquerystring
>>
>> Can I pique anyone's interests in getting this resolved? It seems an easy
>> win.
>>
> Hi - perhaps if you were to provide a patch of the proposed new class
> with updated copyright and package name etc then you might get a bite.
>
>
>> Regards,
>>
>> Richard.
>>
>>
>
>
>
/*
* Copyright (c) 2000, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package java.net.URLEncodedQueryString;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.net.URLEncodedQueryString;
import java.net.URLEncodedQueryString.Separator;
/**
* Unit tests for URLEncodedQueryString
*
* @author Richard Kennard
* @version 1.2
*/
public class URLEncodedQueryStringTests {
public static void main( String[] args )
throws Exception {
testGetters();
testSetters();
testApply();
testEquals();
testRoundTrip();
testURLEncodedParameterNames();
}
/**
* Test getters
*/
public static void testGetters() {
URLEncodedQueryString queryString =
URLEncodedQueryString.parse( "id=1" );
assertEquals( "id=1", queryString.toString() );
assertEquals( "id=1", queryString.toString() );
queryString = URLEncodedQueryString.parse( "x=1&y=2" );
assertEquals( "x=1&y=2", queryString.toString() );
assertEquals( "x=1;y=2", queryString.toString(
Separator.SEMICOLON ) );
assertEquals( "1", queryString.get( "x" ) );
assertEquals( "2", queryString.getValues( "y" ).get( 0 ) );
assertEquals( "2", queryString.getMap().get( "y" ).get( 0 ) );
assertEquals( queryString.get( "z" ), null );
assertTrue( !queryString.contains( "z" ) );
Iterator<String> i = queryString.getNames();
assertEquals( "x", i.next() );
assertEquals( "y", i.next() );
assertTrue( !i.hasNext() );
// contains
queryString = URLEncodedQueryString.parse( "x=1&y=2&z" );
assertEquals( queryString.get( "z" ), null );
assertTrue( queryString.contains( "z" ) );
}
/**
* Test setters
*/
public static void testSetters()
throws URISyntaxException {
// New parameter
URLEncodedQueryString queryString =
URLEncodedQueryString.create();
queryString.set( "forumId", 3 );
assertEquals( "forumId=3", queryString.toString() );
queryString.set( "forumId", (Number) null );
assertEquals( "", queryString.toString() );
try {
queryString.set( null, "3" );
assertTrue( false );
} catch ( NullPointerException e ) {
// Should fail
}
try {
queryString.set( null, (String) null );
assertTrue( false );
} catch ( NullPointerException e ) {
// Should fail
}
queryString.set( "name", "Richard Kennard" );
assertEquals( "name=Richard+Kennard", queryString.toString() );
queryString.append( "name", "Duey Kennard" );
assertEquals( "name=Richard+Kennard&name=Duey+Kennard",
queryString.toString() );
queryString.append( "name", (String) null ).append( null );
assertEquals( "name=Richard+Kennard&name=Duey+Kennard&name",
queryString.toString() );
queryString.append( "name=Huey+Kennard&name=Millie+Kennard" );
assertEquals(
"name=Richard+Kennard&name=Duey+Kennard&name&name=Huey+Kennard&name=Millie+Kennard",
queryString.toString() );
queryString.set( "name=Huey+Kennard;name=Millie+Kennard;add" );
assertEquals( "name=Huey+Kennard&name=Millie+Kennard&add",
queryString.toString() );
queryString.remove( "name" );
assertEquals( "add", queryString.toString() );
assertTrue( !queryString.isEmpty() );
queryString.remove( "add" );
assertTrue( queryString.isEmpty() );
queryString = URLEncodedQueryString.parse( new URI(
"http://java.com?a=%3C%3E%26&b=2" ) );
assertEquals( "<>&", queryString.get( "a" ) );
Map<String, List<String>> queryMap = queryString.getMap();
queryMap.get( "a" ).add( 0, "foo" );
queryMap.put( "b", new ArrayList<String>( Arrays.asList( "3" )
) );
// (should not have modified original)
assertEquals( "a=%3C%3E%26&b=2", queryString.toString() );
queryString = URLEncodedQueryString.create(
queryString.getMap() );
assertEquals( "a=%3C%3E%26&b=2", queryString.toString() );
queryMap.get( "a" ).add( 0, "foo" );
assertEquals( "a=%3C%3E%26&b=2", queryString.toString() );
// Test round-trip
queryString = URLEncodedQueryString.create();
queryString.set( "a", "x&y" );
queryString.set( "b", "u;v" );
assertEquals( "a=x%26y&b=u%3Bv", queryString.toString() );
queryString = URLEncodedQueryString.parse(
queryString.toString() );
assertEquals( "x&y", queryString.get( "a" ) );
assertEquals( "u;v", queryString.get( "b" ) );
}
/**
* Test apply
*/
public static void testApply()
throws URISyntaxException {
URI uri = new URI( "http://java.com?page=1" );
URLEncodedQueryString queryString =
URLEncodedQueryString.parse( uri );
queryString.set( "page", 2 );
uri = queryString.apply( uri );
assertEquals( "http://java.com?page=2", uri.toString() );
uri = new URI( "/forum.jsp?message=12" );
queryString = URLEncodedQueryString.parse( uri ).append(
"reply", 2 );
uri = queryString.apply( uri );
assertEquals( "/forum.jsp?message=12&reply=2", uri.toString() );
// Test escaping
uri = new URI( "http://www.oracle.com/search?q=foo+bar" );
queryString = URLEncodedQueryString.parse( uri );
queryString.set( "q", "100%" );
uri = queryString.apply( uri );
assertEquals( "http://www.oracle.com/search?q=100%25",
uri.toString() );
queryString.append( "%", "%25" );
uri = queryString.apply( uri );
assertEquals(
"http://www.oracle.com/search?q=100%25&%25=%2525", uri.toString() );
queryString.set( "q", "a + b = 100%" );
queryString.remove( "%" );
uri = queryString.apply( uri );
assertEquals(
"http://www.oracle.com/search?q=a+%2B+b+%3D+100%25", uri.toString() );
// Test different parts of the URI
uri = new URI( "http://[email protected]:80#bar" );
uri = queryString.apply( uri );
assertEquals(
"http://[email protected]:80?q=a+%2B+b+%3D+100%25#bar", uri.toString() );
uri = new URI( "http", "userinfo", "::192.9.5.5", 8080,
"/path", "query", "fragment" );
uri = queryString.apply( uri );
assertEquals(
"http://userinfo@[::192.9.5.5]:8080/path?q=a+%2B+b+%3D+100%25#fragment",
uri.toString() );
uri = new URI( "http", "userinfo", "[::192.9.5.5]", 8080,
"/path", "query", "fragment" );
uri = queryString.apply( uri );
assertEquals(
"http://userinfo@[::192.9.5.5]:8080/path?q=a+%2B+b+%3D+100%25#fragment",
uri.toString() );
uri = new URI( "file", "/authority", null, null, null );
uri = queryString.apply( uri );
assertEquals( "file:///authority?q=a+%2B+b+%3D+100%25",
uri.toString() );
}
/**
* Test equals
*/
public static void testEquals()
throws Exception {
URI uri = new URI( "http://java.com?page=1¶=2" );
URLEncodedQueryString queryString =
URLEncodedQueryString.parse( uri );
assertEquals( queryString, queryString );
assertTrue( !queryString.equals( uri ) );
URLEncodedQueryString queryString2 =
URLEncodedQueryString.create();
assertTrue( !queryString.equals( queryString2 ) );
assertTrue( !queryString2.equals( queryString ) );
queryString2 = URLEncodedQueryString.parse( uri.getQuery() );
assertEquals( queryString, queryString2 );
assertTrue( queryString.hashCode() == queryString2.hashCode() );
queryString.set( "page", 2 );
assertTrue( !queryString.equals( queryString2 ) );
assertTrue( queryString.hashCode() != queryString2.hashCode() );
queryString = URLEncodedQueryString.create();
queryString2 = URLEncodedQueryString.create();
assertEquals( queryString, queryString2 );
assertTrue( queryString.hashCode() == queryString2.hashCode() );
}
/**
* Test round-trip
*/
public static void testRoundTrip()
throws Exception {
assertEquals( "page=1¶=2", URLEncodedQueryString.parse(
"page=1¶=2" ).toString() );
assertEquals( "bar=&baz", URLEncodedQueryString.parse(
"bar=&baz" ).toString() );
assertEquals( "bar=1&bar=2&bar&bar=&bar=3",
URLEncodedQueryString.parse( "bar=1&bar=2&bar&bar=&bar=3" ).toString() );
}
public static void testURLEncodedParameterNames() {
assertEquals( "page=1¶=2", URLEncodedQueryString.parse(
"%70age=1&par%61=2" ).toString() );
}
//
// Private statics
//
private static void assertTrue( boolean expectedTrue ) {
if ( !expectedTrue ) {
throw new RuntimeException( "False" );
}
}
private static void assertEquals( Object expected, Object actual ) {
if ( expected == null ) {
if ( actual != null ) {
throw new RuntimeException( "Expected null, but
got: " + actual );
}
} else if ( !expected.equals( actual ) ) {
throw new RuntimeException( "Expected: " + expected +
", but got: " + actual );
}
}
}
/*
* Copyright (c) 1998, 2006, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package java.net;
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 containing an (ordered) list
of parameters.
* <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:
* <p>
* <code>
* URI uri = new URI("http://java.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&para=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.2
*/
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 static final 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. Returns
<code>null</code> if the
* parameter does not exist, or exists but has a <code>null</code>
value (see {@link #contains
* contains}).
* <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 or exists
but with a null value
* (see {@link #contains contains}).
*/
public String get( final String name ) {
List<String> parameters = getValues( name );
if ( parameters == null || parameters.isEmpty() ) {
return null;
}
return parameters.get( 0 );
}
/**
* Returns whether the named parameter exists.
* <p>
* This can be useful to distinguish between a parameter not existing,
and a parameter existing
* but with a <code>null</code> value (eg. <code>foo=1&bar</code>).
This is distinct from a
* parameter existing with a value of the empty String (eg.
<code>foo=1&bar=</code>).
*/
public boolean contains( final String name ) {
return this.queryMap.containsKey( name );
}
/**
* 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 == null ) {
return false;
}
if ( getClass() != obj.getClass() ) {
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 name : this.queryMap.keySet() ) {
for ( String value : this.queryMap.get( name ) ) {
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(
name, "UTF-8" ) );
if ( value != null ) {
builder.append( '=' );
builder.append(
URLEncoder.encode( value, "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 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 name;
String value;
try {
if ( indexOf == -1 ) {
name = parameter;
value = null;
} else {
name = parameter.substring( 0, indexOf
);
value = parameter.substring( indexOf +
1 );
}
// Decode the name if necessary (i.e. %70age=1
becomes page=1)
name = URLDecoder.decode( name, "UTF-8" );
// 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( name )
) {
remove( name );
}
setAlreadyParsed.add( name );
}
if ( value != null ) {
value = URLDecoder.decode( value,
"UTF-8" );
}
appendOrSet( name, value, true );
} catch ( UnsupportedEncodingException e ) {
// Should never happen. UTF-8 should always be
available
// according to Java spec
throw new RuntimeException( e );
}
}
}
}