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://rkenn...@java.com:80#bar"; );
                uri = queryString.apply( uri );
                assertEquals( 
"http://rkenn...@java.com: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&para=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&para=2", URLEncodedQueryString.parse( 
"page=1&para=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&para=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&amp;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 "&amp;" 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>&amp;</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 '&amp;' 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( '&', '&amp;' ))
        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 );
                        }
                }
        }
}

Reply via email to