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¶=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 ); } } } }