Fix for issue 3374. This patch adds overloads of encodeComponent and
decodeComponent to deal differently with spaces (keeping them as %20
rather than replacing them with +) and therefore allow their use for
path segments (useful when using "RESTful services").

http://gwt-code-reviews.appspot.com/87806

Patch by: t.broyer@gmail.com
Review by: rjrjr@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6470 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/http/client/URL.java b/user/src/com/google/gwt/http/client/URL.java
index 79d3697..b498447 100644
--- a/user/src/com/google/gwt/http/client/URL.java
+++ b/user/src/com/google/gwt/http/client/URL.java
@@ -58,6 +58,25 @@
   }
 
   /**
+   * Returns a string where all URL component escape sequences have been
+   * converted back to their original character representations.
+   * 
+   * @param encodedURLComponent string containing encoded URL component
+   *        sequences
+   * @param fromQueryString if <code>true</code>, +'s will be turned into
+   *        spaces, otherwise they'll be kept as-is.
+   * @return string with no encoded URL component encoded sequences
+   * 
+   * @throws NullPointerException if encodedURLComponent is <code>null</code>
+   */
+  public static String decodeComponent(String encodedURLComponent,
+      boolean fromQueryString) {
+    StringValidator.throwIfNull("encodedURLComponent", encodedURLComponent);
+    return fromQueryString ? decodeComponentImpl(encodedURLComponent)
+        : decodeComponentRawImpl(encodedURLComponent);
+  }
+
+  /**
    * Returns a string where all characters that are not valid for a complete URL
    * have been escaped. The escaping of a character is done by converting it
    * into its UTF-8 encoding and then encoding each of the resulting bytes as a
@@ -131,6 +150,44 @@
     return encodeComponentImpl(decodedURLComponent);
   }
 
+  /**
+   * Returns a string where all characters that are not valid for a URL
+   * component have been escaped. The escaping of a character is done by
+   * converting it into its UTF-8 encoding and then encoding each of the
+   * resulting bytes as a %xx hexadecimal escape sequence.
+   * 
+   * <p>
+   * The following character sets are <em>not</em> escaped by this method:
+   * <ul>
+   * <li>ASCII digits or letters</li>
+   * <li>ASCII punctuation characters: <pre>- _ . ! ~ * ' ( )</pre></li>
+   * </ul>
+   * </p>
+   * 
+   * <p>
+   * Notice that this method <em>does</em> encode the URL component delimiter
+   * characters:<blockquote>
+   * 
+   * <pre>
+   * ; / ? : &amp; = + $ , #
+   * </pre>
+   * 
+   * </blockquote>
+   * </p>
+   * 
+   * @param decodedURLComponent a string containing invalid URL characters
+   * @param queryStringSpaces if <code>true</code>, spaces will be encoded as +'s.
+   * @return a string with all invalid URL characters escaped
+   * 
+   * @throws NullPointerException if decodedURLComponent is <code>null</code>
+   */
+  public static String encodeComponent(String decodedURLComponent,
+      boolean queryStringSpaces) {
+    StringValidator.throwIfNull("decodedURLComponent", decodedURLComponent);
+    return queryStringSpaces ? encodeComponentImpl(decodedURLComponent)
+        : encodeComponentRawImpl(decodedURLComponent);
+  }
+
   /*
    * Note: this method will convert the space character escape short form, '+',
    * into a space.
@@ -140,6 +197,10 @@
     return decodeURIComponent(encodedURLComponent.replace(regexp, "%20"));    
   }-*/;
 
+  private static native String decodeComponentRawImpl(String encodedURLComponent) /*-{
+    return decodeURIComponent(encodedURLComponent);
+  }-*/;
+
   private static native String decodeImpl(String encodedURL) /*-{
     return decodeURI(encodedURL);
   }-*/;
@@ -153,6 +214,10 @@
     return encodeURIComponent(decodedURLComponent).replace(regexp, "+");
    }-*/;
 
+  private static native String encodeComponentRawImpl(String decodedURLComponent) /*-{
+    return encodeURIComponent(decodedURLComponent);
+   }-*/;
+
   private static native String encodeImpl(String decodedURL) /*-{
     return encodeURI(decodedURL);
   }-*/;
diff --git a/user/test/com/google/gwt/http/client/URLTest.java b/user/test/com/google/gwt/http/client/URLTest.java
index 3b07b7b..417e888 100644
--- a/user/test/com/google/gwt/http/client/URLTest.java
+++ b/user/test/com/google/gwt/http/client/URLTest.java
@@ -22,10 +22,11 @@
  */
 public class URLTest extends GWTTestCase {
 
-  private final String DECODED_URL = "http://www.foo \u00E9 bar.com/1_!~*'();/?@&=+$,#";
-  private final String DECODED_URL_COMPONENT = "-_.!~*'():/#?@ \u00E9 ";
-  private final String ENCODED_URL = "http://www.foo%20%C3%A9%20bar.com/1_!~*'();/?@&=+$,#";
-  private final String ENCODED_URL_COMPONENT = "-_.!~*'()%3A%2F%23%3F%40+%C3%A9+";
+  private final String DECODED_URL = "http://www.foo \u00E9+bar.com/1_!~*'();/?@&=+$,#";
+  private final String DECODED_URL_COMPONENT = "-_.!~*'():/#?@ \u00E9+";
+  private final String ENCODED_URL = "http://www.foo%20%C3%A9+bar.com/1_!~*'();/?@&=+$,#";
+  private final String ENCODED_URL_COMPONENT = "-_.!~*'()%3A%2F%23%3F%40%20%C3%A9%2B";
+  private final String ENCODED_URL_COMPONENT_QS = "-_.!~*'()%3A%2F%23%3F%40+%C3%A9%2B";
 
   public String getModuleName() {
     return "com.google.gwt.http.HttpSuite";
@@ -67,6 +68,37 @@
 
     String actualURLComponent = URL.decodeComponent(ENCODED_URL_COMPONENT);
     assertEquals(DECODED_URL_COMPONENT, actualURLComponent);
+
+    actualURLComponent = URL.decodeComponent(ENCODED_URL_COMPONENT_QS);
+    assertEquals(DECODED_URL_COMPONENT, actualURLComponent);
+  }
+
+  /**
+   * Test method for
+   * {@link com.google.gwt.http.client.URL#decodeComponent(java.lang.String,boolean)}.
+   */
+  public void testDecodeComponent2() {
+    try {
+      URL.decodeComponent(null);
+      fail("Expected NullPointerException");
+    } catch (NullPointerException ex) {
+      // expected exception was thrown
+    }
+
+    assertEquals("", URL.decodeComponent("", false));
+    assertEquals("", URL.decodeComponent("", true));
+    assertEquals(" ", URL.decodeComponent(" ", false));
+    assertEquals(" ", URL.decodeComponent(" ", true));
+    assertEquals("+", URL.decodeComponent("+", false));
+    assertEquals(" ", URL.decodeComponent("+", true));
+    assertEquals(" ", URL.decodeComponent("%20", false));
+    assertEquals(" ", URL.decodeComponent("%20", true));
+
+    String actualURLComponent = URL.decodeComponent(ENCODED_URL_COMPONENT, false);
+    assertEquals(DECODED_URL_COMPONENT, actualURLComponent);
+
+    actualURLComponent = URL.decodeComponent(ENCODED_URL_COMPONENT_QS, true);
+    assertEquals(DECODED_URL_COMPONENT, actualURLComponent);
   }
 
   /**
@@ -104,6 +136,37 @@
     assertEquals("+", URL.encodeComponent(" "));
 
     String actualURLComponent = URL.encodeComponent(DECODED_URL_COMPONENT);
+    assertEquals(ENCODED_URL_COMPONENT_QS, actualURLComponent);
+  }
+
+  /**
+   * Test method for
+   * {@link com.google.gwt.http.client.URL#encodeComponent(java.lang.String,boolean)}.
+   */
+  public void testEncodeComponent2() {
+    try {
+      URL.encodeComponent(null, false);
+      fail("Expected NullPointerException");
+    } catch (NullPointerException ex) {
+      // expected exception was thrown
+    }
+
+    try {
+      URL.encodeComponent(null, true);
+      fail("Expected NullPointerException");
+    } catch (NullPointerException ex) {
+      // expected exception was thrown
+    }
+
+    assertEquals("", URL.encodeComponent("", false));
+    assertEquals("", URL.encodeComponent("", true));
+    assertEquals("%20", URL.encodeComponent(" ", false));
+    assertEquals("+", URL.encodeComponent(" ", true));
+
+    String actualURLComponent = URL.encodeComponent(DECODED_URL_COMPONENT, false);
     assertEquals(ENCODED_URL_COMPONENT, actualURLComponent);
+
+    actualURLComponent = URL.encodeComponent(DECODED_URL_COMPONENT, true);
+    assertEquals(ENCODED_URL_COMPONENT_QS, actualURLComponent);
   }
 }