Recommitting r6329 to add UrlBuilder, this time with passing tests.

Patch by: jlabanca
Review by: rjrjr (incremental)

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6350 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/Showcase.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/Showcase.java
index bf5a417..fc29093 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/Showcase.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/Showcase.java
@@ -29,6 +29,7 @@
 import com.google.gwt.event.logical.shared.SelectionHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.http.client.UrlBuilder;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.sample.showcase.client.content.i18n.CwConstantsExample;
 import com.google.gwt.sample.showcase.client.content.i18n.CwConstantsWithLookupExample;
@@ -69,6 +70,7 @@
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.Window.Location;
 import com.google.gwt.user.client.ui.AbstractImagePrototype;
 import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
@@ -151,29 +153,6 @@
   static String CUR_THEME = ShowcaseConstants.STYLE_THEMES[0];
 
   /**
-   * Get the URL of the page, without an hash of query string.
-   * 
-   * @return the location of the page
-   */
-  private static native String getHostPageLocation()
-  /*-{
-    var s = $doc.location.href;
-
-    // Pull off any hash.
-    var i = s.indexOf('#');
-    if (i != -1)
-      s = s.substring(0, i);
-
-    // Pull off any query string.
-    i = s.indexOf('?');
-    if (i != -1)
-      s = s.substring(0, i);
-
-    // Ensure a final slash if non-empty.
-    return s;
-  }-*/;
-
-  /**
    * The {@link Application}.
    */
   private Application app = new Application();
@@ -449,8 +428,9 @@
     localeBox.addChangeHandler(new ChangeHandler() {
       public void onChange(ChangeEvent event) {
         String localeName = localeBox.getValue(localeBox.getSelectedIndex());
-        Window.open(getHostPageLocation() + "?locale=" + localeName, "_self",
-            "");
+        UrlBuilder builder = Location.createUrlBuilder().setParameter("locale",
+            localeName);
+        Window.Location.replace(builder.buildString());
       }
     });
     HorizontalPanel localeWrapper = new HorizontalPanel();
diff --git a/user/src/com/google/gwt/http/client/UrlBuilder.java b/user/src/com/google/gwt/http/client/UrlBuilder.java
new file mode 100644
index 0000000..3edd2bb
--- /dev/null
+++ b/user/src/com/google/gwt/http/client/UrlBuilder.java
@@ -0,0 +1,243 @@
+/*

+ * Copyright 2009 Google Inc.

+ * 

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not

+ * use this file except in compliance with the License. You may obtain a copy of

+ * the License at

+ * 

+ * http://www.apache.org/licenses/LICENSE-2.0

+ * 

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

+ * License for the specific language governing permissions and limitations under

+ * the License.

+ */

+package com.google.gwt.http.client;

+

+import java.util.HashMap;

+import java.util.Map;

+

+/**

+ * Utility class to build a URL from components.

+ * 

+ * TODO(jlabanca): Add a constructor that parses an existing URL

+ */

+public class UrlBuilder {

+

+  /**

+   * The port to use when no port should be specified.

+   */

+  public static final int PORT_UNSPECIFIED = Integer.MIN_VALUE;

+

+  /**

+   * A mapping of query parameters to their values.

+   */

+  private Map<String, String[]> listParamMap = new HashMap<String, String[]>();

+

+  private String protocol = "http";

+  private String host = null;

+  private int port = PORT_UNSPECIFIED;

+  private String path = null;

+  private String hash = null;

+

+  /**

+   * Build the URL and return it as an encoded string.

+   * 

+   * @return the encoded URL string

+   */

+  public String buildString() {

+    StringBuilder url = new StringBuilder();

+

+    // http://

+    url.append(protocol).append("://");

+

+    // http://www.google.com

+    if (host != null) {

+      url.append(host);

+    }

+

+    // http://www.google.com:80

+    if (port != PORT_UNSPECIFIED) {

+      url.append(":").append(port);

+    }

+

+    // http://www.google.com:80/path/to/file.html

+    if (path != null && !"".equals(path)) {

+      url.append("/").append(path);

+    }

+

+    // Generate the query string.

+    // http://www.google.com:80/path/to/file.html?k0=v0&k1=v1

+    char prefix = '?';

+    for (Map.Entry<String, String[]> entry : listParamMap.entrySet()) {

+      for (String val : entry.getValue()) {

+        url.append(prefix).append(entry.getKey()).append('=');

+        if (val != null) {

+          url.append(val);

+        }

+        prefix = '&';

+      }

+    }

+

+    // http://www.google.com:80/path/to/file.html?k0=v0&k1=v1#token

+    if (hash != null) {

+      url.append("#").append(hash);

+    }

+

+    return URL.encode(url.toString());

+  }

+

+  /**

+   * Remove a query parameter from the map.

+   * 

+   * @param name the parameter name

+   */

+  public UrlBuilder removeParameter(String name) {

+    listParamMap.remove(name);

+    return this;

+  }

+

+  /**

+   * Set the hash portion of the location (ex. myAnchor or #myAnchor).

+   * 

+   * @param hash the hash

+   */

+  public UrlBuilder setHash(String hash) {

+    if (hash != null && hash.startsWith("#")) {

+      hash = hash.substring(1);

+    }

+    this.hash = hash;

+    return this;

+  }

+

+  /**

+   * Set the host portion of the location (ex. google.com). You can also specify

+   * the port in this method (ex. localhost:8888).

+   * 

+   * @param host the host

+   */

+  public UrlBuilder setHost(String host) {

+    // Extract the port from the host.

+    if (host != null && host.contains(":")) {

+      String[] parts = host.split(":");

+      if (parts.length > 2) {

+        throw new IllegalArgumentException(

+            "Host contains more than one colon: " + host);

+      }

+      try {

+        setPort(Integer.parseInt(parts[1]));

+      } catch (NumberFormatException e) {

+        throw new IllegalArgumentException("Could not parse port out of host: "

+            + host);

+      }

+      host = parts[0];

+    }

+    this.host = host;

+    return this;

+  }

+

+  /**

+   * <p>

+   * Set a query parameter to a list of values. Each value in the list will be

+   * added as its own key/value pair.

+   * 

+   * <p>

+   * <h3>Example Output</h3>

+   * <code>?mykey=value0&mykey=value1&mykey=value2</code>

+   * </p>

+   * 

+   * @param key the key

+   * @param values the list of values

+   */

+  public UrlBuilder setParameter(String key, String... values) {

+    assertNotNullOrEmpty(key, "Key cannot be null or empty", false);

+    assertNotNull(values,

+        "Values cannot null. Try using removeParameter instead.");

+    if (values.length == 0) {

+      throw new IllegalArgumentException(

+          "Values cannot be empty.  Try using removeParameter instead.");

+    }

+    listParamMap.put(key, values);

+    return this;

+  }

+

+  /**

+   * Set the path portion of the location (ex. path/to/file.html).

+   * 

+   * @param path the path

+   */

+  public UrlBuilder setPath(String path) {

+    if (path != null && path.startsWith("/")) {

+      path = path.substring(1);

+    }

+    this.path = path;

+    return this;

+  }

+

+  /**

+   * Set the port to connect to.

+   * 

+   * @param port the port, or {@link #PORT_UNSPECIFIED}

+   */

+  public UrlBuilder setPort(int port) {

+    this.port = port;

+    return this;

+  }

+

+  /**

+   * Set the protocol portion of the location (ex. http).

+   * 

+   * @param protocol the protocol

+   */

+  public UrlBuilder setProtocol(String protocol) {

+    assertNotNull(protocol, "Protocol cannot be null");

+    if (protocol.endsWith("://")) {

+      protocol = protocol.substring(0, protocol.length() - 3);

+    } else if (protocol.endsWith(":/")) {

+      protocol = protocol.substring(0, protocol.length() - 2);

+    } else if (protocol.endsWith(":")) {

+      protocol = protocol.substring(0, protocol.length() - 1);

+    }

+    if (protocol.contains(":")) {

+      throw new IllegalArgumentException("Invalid protocol: " + protocol);

+    }

+    assertNotNullOrEmpty(protocol, "Protocol cannot be empty", false);

+    this.protocol = protocol;

+    return this;

+  }

+

+  /**

+   * Assert that the value is not null.

+   * 

+   * @param value the value

+   * @param message the message to include with any exceptions

+   * @throws IllegalArgumentException if value is null

+   */

+  private void assertNotNull(Object value, String message)

+      throws IllegalArgumentException {

+    if (value == null) {

+      throw new IllegalArgumentException(message);

+    }

+  }

+

+  /**

+   * Assert that the value is not null or empty.

+   * 

+   * @param value the value

+   * @param message the message to include with any exceptions

+   * @param isState if true, throw a state exception instead

+   * @throws IllegalArgumentException if value is null

+   * @throws IllegalStateException if value is null and isState is true

+   */

+  private void assertNotNullOrEmpty(String value, String message,

+      boolean isState) throws IllegalArgumentException {

+    if (value == null || value.length() == 0) {

+      if (isState) {

+        throw new IllegalStateException(message);

+      } else {

+        throw new IllegalArgumentException(message);

+      }

+    }

+  }

+}

diff --git a/user/src/com/google/gwt/user/client/Window.java b/user/src/com/google/gwt/user/client/Window.java
index 7b6b25b..022d1ff 100644
--- a/user/src/com/google/gwt/user/client/Window.java
+++ b/user/src/com/google/gwt/user/client/Window.java
@@ -30,6 +30,7 @@
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.event.shared.HasHandlers;
 import com.google.gwt.http.client.URL;
+import com.google.gwt.http.client.UrlBuilder;
 import com.google.gwt.user.client.impl.WindowImpl;
 
 import java.util.ArrayList;
@@ -132,6 +133,39 @@
     }-*/;
 
     /**
+     * Create a {@link UrlBuilder} based on this {@link Location}.
+     * 
+     * @return the new builder
+     */
+    public static UrlBuilder createUrlBuilder() {
+      UrlBuilder builder = new UrlBuilder();
+      builder.setProtocol(getProtocol());
+      builder.setHost(getHost());
+      String path = getPath();
+      if (path != null && path.length() > 0) {
+        builder.setPath(path);
+      }
+      String hash = getHash();
+      if (hash != null && hash.length() > 0) {
+        builder.setHash(hash);
+      }
+      String port = getPort();
+      if (port != null && port.length() > 0) {
+        builder.setPort(Integer.parseInt(port));
+      }
+
+      // Add query parameters.
+      Map<String, List<String>> params = getParameterMap();
+      for (Map.Entry<String, List<String>> entry : params.entrySet()) {
+        List<String> values = new ArrayList<String>(entry.getValue());
+        builder.setParameter(entry.getKey(),
+            values.toArray(new String[values.size()]));
+      }
+
+      return builder;
+    }
+
+    /**
      * Gets the string to the right of the URL's hash.
      * 
      * @return the string to the right of the URL's hash.
@@ -400,7 +434,7 @@
   private static boolean resizeHandlersInitialized;
   private static int lastResizeWidth;
   private static int lastResizeHeight;
-  
+
   private static final WindowImpl impl = GWT.create(WindowImpl.class);
 
   /**
@@ -478,7 +512,8 @@
    * Adds a listener to receive window scroll events.
    * 
    * @param listener the listener to be informed when the window is scrolled
-   * @deprecated use {@link Window#addWindowScrollHandler(ScrollHandler)} instead
+   * @deprecated use {@link Window#addWindowScrollHandler(ScrollHandler)}
+   *             instead
    */
   @Deprecated
   public static void addWindowScrollListener(WindowScrollListener listener) {
diff --git a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
index 226ec72..9fdaa8d 100644
--- a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
+++ b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.UrlBuilder;
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
@@ -257,18 +258,11 @@
        * We're being asked to run a test in a different module. We must navigate
        * the browser to a new URL which will run that other module.
        */
-      String href = Window.Location.getHref();
-      if (href.contains("?")) {
-        href = href.substring(0, href.indexOf("?"));
-      } else if (href.contains("#")) {
-        href = href.substring(0, href.indexOf("#"));
-      }
-      String newHref = href.replace(currentModule, newModule);
-      newHref += "?" + BLOCKINDEX_QUERY_PARAM + "=" + currentBlock.getIndex();
-      if (maxRetryCount >= 0) {
-        newHref += "&" + RETRYCOUNT_QUERY_PARAM + "=" + maxRetryCount;
-      }
-      Window.Location.replace(newHref);
+      UrlBuilder builder = Window.Location.createUrlBuilder();
+      builder.setParameter(BLOCKINDEX_QUERY_PARAM,
+          Integer.toString(currentBlock.getIndex())).setPath(
+          newModule + "/junit.html");
+      Window.Location.replace(builder.buildString());
       currentBlock = null;
       currentTestIndex = 0;
     }
diff --git a/user/test/com/google/gwt/http/HTTPSuite.java b/user/test/com/google/gwt/http/HTTPSuite.java
index 905f173..e5b0557 100644
--- a/user/test/com/google/gwt/http/HTTPSuite.java
+++ b/user/test/com/google/gwt/http/HTTPSuite.java
@@ -20,6 +20,7 @@
 import com.google.gwt.http.client.RequestTest;
 import com.google.gwt.http.client.ResponseTest;
 import com.google.gwt.http.client.URLTest;
+import com.google.gwt.http.client.UrlBuilderTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
 
 import junit.framework.Test;
@@ -38,6 +39,7 @@
     suite.addTestSuite(RequestBuilderTest.class);
     suite.addTestSuite(RequestTest.class);
     suite.addTestSuite(ResponseTest.class);
+    suite.addTestSuite(UrlBuilderTest.class);
 
     return suite;
   }
diff --git a/user/test/com/google/gwt/http/client/UrlBuilderTest.java b/user/test/com/google/gwt/http/client/UrlBuilderTest.java
new file mode 100644
index 0000000..51496dc
--- /dev/null
+++ b/user/test/com/google/gwt/http/client/UrlBuilderTest.java
@@ -0,0 +1,335 @@
+/*

+ * Copyright 2009 Google Inc.

+ * 

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not

+ * use this file except in compliance with the License. You may obtain a copy of

+ * the License at

+ * 

+ * http://www.apache.org/licenses/LICENSE-2.0

+ * 

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

+ * License for the specific language governing permissions and limitations under

+ * the License.

+ */

+package com.google.gwt.http.client;

+

+import com.google.gwt.junit.client.GWTTestCase;

+

+import java.util.ArrayList;

+import java.util.List;

+

+/**

+ * Test Case for {@link UrlBuilder}.

+ */

+public class UrlBuilderTest extends GWTTestCase {

+

+  @Override

+  public String getModuleName() {

+    return "com.google.gwt.http.HttpSuite";

+  }

+

+  /**

+   * Test that the URL is encoded correctly.

+   */

+  public void testBuildStringEncode() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+    builder.setPath("path to file");

+    builder.setParameter("the key", "the value");

+    assertEquals("http://google.com/path%20to%20file?the%20key=the%20value",

+        builder.buildString());

+  }

+

+  public void testBuildStringEntireUrl() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+

+    // Host only.

+    assertEquals("http://google.com", builder.buildString());

+

+    // Host:Port

+    builder.setPort(100);

+    assertEquals("http://google.com:100", builder.buildString());

+

+    // Host:Port/Path

+    builder.setPath("path/to/file");

+    assertEquals("http://google.com:100/path/to/file", builder.buildString());

+

+    // Host:Port/Path?Param

+    builder.setParameter("key", "value");

+    assertEquals("http://google.com:100/path/to/file?key=value",

+        builder.buildString());

+

+    // Host:Port/Path?Param#Hash

+    builder.setHash("token");

+    assertEquals("http://google.com:100/path/to/file?key=value#token",

+        builder.buildString());

+  }

+

+  public void testBuildStringEntireUrlWithReturns() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com").setPort(100).setPath("path/to/file").setParameter(

+        "key", "value").setHash("token");

+    assertEquals("http://google.com:100/path/to/file?key=value#token",

+        builder.buildString());

+  }

+

+  public void testBuildStringParts() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+

+    // Host only.

+    assertEquals("http://google.com", builder.buildString());

+

+    // Host:Port

+    builder.setPort(100);

+    assertEquals("http://google.com:100", builder.buildString());

+    builder.setPort(UrlBuilder.PORT_UNSPECIFIED);

+

+    // Host/Path

+    builder.setPath("path/to/file");

+    assertEquals("http://google.com/path/to/file", builder.buildString());

+    builder.setPath(null);

+

+    // Host?Param

+    builder.setParameter("key", "value");

+    assertEquals("http://google.com?key=value", builder.buildString());

+    builder.removeParameter("key");

+

+    // Host#Hash

+    builder.setHash("token");

+    assertEquals("http://google.com#token", builder.buildString());

+    builder.setHash(null);

+  }

+

+  public void testSetHash() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+

+    // Hash not specified

+    assertEquals("http://google.com", builder.buildString());

+

+    // # added if not present

+    builder.setHash("myHash");

+    assertEquals("http://google.com#myHash", builder.buildString());

+

+    // Null hash

+    builder.setHash(null);

+    assertEquals("http://google.com", builder.buildString());

+

+    // # not added if present

+    builder.setHash("#myHash2");

+    assertEquals("http://google.com#myHash2", builder.buildString());

+  }

+

+  public void testSetHost() {

+    UrlBuilder builder = new UrlBuilder();

+

+    // Host not specified.

+    assertEquals("http://", builder.buildString());

+

+    // Null host.

+    builder.setHost(null);

+    assertEquals("http://", builder.buildString());

+

+    // Empty host.

+    builder.setHost("");

+    assertEquals("http://", builder.buildString());

+

+    // google.com

+    builder.setHost("google.com");

+    assertEquals("http://google.com", builder.buildString());

+

+    // google.com:80

+    builder.setHost("google.com:80");

+    assertEquals("http://google.com:80", builder.buildString());

+

+    // google.com:80 with overridden port.

+    builder.setHost("google.com:80");

+    builder.setPort(1000);

+    assertEquals("http://google.com:1000", builder.buildString());

+

+    // google.com:80 with overridden port in host.

+    builder.setPort(1000);

+    builder.setHost("google.com:80");

+    assertEquals("http://google.com:80", builder.buildString());

+

+    // Specify to many ports.

+    // google.com:80:90

+    try {

+      builder.setHost("google.com:80:90");

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+

+    // Specify invalid port.

+    // google.com:test

+    try {

+      builder.setHost("google.com:test");

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+  }

+

+  public void testSetParameter() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+

+    // Parameters not specified.

+    assertEquals("http://google.com", builder.buildString());

+

+    // Simple parameter.

+    builder.setParameter("key", "value");

+    assertEquals("http://google.com?key=value", builder.buildString());

+

+    // Remove simple parameter.

+    builder.removeParameter("key");

+    assertEquals("http://google.com", builder.buildString());

+

+    // List parameter.

+    List<String> values = new ArrayList<String>();

+    builder.setParameter("key", "value0", "value1", "value2");

+    assertEquals("http://google.com?key=value0&key=value1&key=value2",

+        builder.buildString());

+

+    // Remove list parameter.

+    builder.removeParameter("key");

+    assertEquals("http://google.com", builder.buildString());

+

+    // Multiple parameters.

+    builder.setParameter("key0", "value0", "value1", "value2");

+    builder.setParameter("key1", "simpleValue");

+

+    // The order of query params is not defined, so either URL is acceptable.

+    String url = builder.buildString();

+    assertTrue(url.equals("http://google.com?key0=value0&key0=value1&key0=value2&key1=simpleValue")

+        || url.equals("http://google.com?key1=simpleValue&key0=value0&key0=value1&key0=value2"));

+

+    // Empty list of multiple parameters.

+    builder.setParameter("key0", "value0", "value1", "value2");

+    builder.setParameter("key1", "simpleValue");

+    assertTrue(url.equals("http://google.com?key0=value0&key0=value1&key0=value2&key1=simpleValue")

+        || url.equals("http://google.com?key1=simpleValue&key0=value0&key0=value1&key0=value2"));

+  }

+

+  public void testSetParameterToNull() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+

+    try {

+      builder.setParameter(null, "value");

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+

+    try {

+      builder.setParameter(null);

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+

+    try {

+      builder.setParameter("key", new String[0]);

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+

+    try {

+      builder.setParameter("key", (String[]) null);

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+

+    // Null values are okay.

+    builder.setParameter("key", (String) null);

+    assertEquals("http://google.com?key=", builder.buildString());

+  }

+

+  public void testSetPath() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+

+    // Path not specified.

+    assertEquals("http://google.com", builder.buildString());

+

+    // Null path.

+    builder.setPath(null);

+    assertEquals("http://google.com", builder.buildString());

+

+    // Empty path.

+    builder.setPath("");

+    assertEquals("http://google.com", builder.buildString());

+

+    // path/to/file.html

+    builder.setPath("path/to/file.html");

+    assertEquals("http://google.com/path/to/file.html", builder.buildString());

+

+    // /path/to/file.html

+    builder.setPath("/path/to/file.html");

+    assertEquals("http://google.com/path/to/file.html", builder.buildString());

+  }

+

+  public void testSetPort() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+

+    // Port not specified.

+    assertEquals("http://google.com", builder.buildString());

+

+    // Port 1000.

+    builder.setPort(1000);

+    assertEquals("http://google.com:1000", builder.buildString());

+

+    // PORT_UNSPECIFIED.

+    builder.setPort(UrlBuilder.PORT_UNSPECIFIED);

+    assertEquals("http://google.com", builder.buildString());

+  }

+

+  public void testSetProtocol() {

+    UrlBuilder builder = new UrlBuilder();

+    builder.setHost("google.com");

+

+    // Protocol not specified.

+    assertEquals("http://google.com", builder.buildString());

+

+    // Null host.

+    try {

+      builder.setProtocol(null);

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+

+    // Empty host.

+    try {

+      builder.setProtocol("");

+      fail("Expected IllegalArgumentException");

+    } catch (IllegalArgumentException e) {

+      // Expected.

+    }

+

+    // ftp

+    builder.setProtocol("ftp");

+    assertEquals("ftp://google.com", builder.buildString());

+

+    // tcp:

+    builder.setProtocol("tcp:");

+    assertEquals("tcp://google.com", builder.buildString());

+

+    // http:/

+    builder.setProtocol("http:/");

+    assertEquals("http://google.com", builder.buildString());

+

+    // http://

+    builder.setProtocol("http://");

+    assertEquals("http://google.com", builder.buildString());

+  }

+}

diff --git a/user/test/com/google/gwt/user/client/WindowTest.java b/user/test/com/google/gwt/user/client/WindowTest.java
index b50e698..b06029f 100644
--- a/user/test/com/google/gwt/user/client/WindowTest.java
+++ b/user/test/com/google/gwt/user/client/WindowTest.java
@@ -16,9 +16,11 @@
 package com.google.gwt.user.client;
 
 import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.http.client.UrlBuilder;
 import com.google.gwt.junit.DoNotRunWith;
 import com.google.gwt.junit.Platform;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.Window.Location;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.RootPanel;
 
@@ -32,8 +34,8 @@
 public class WindowTest extends GWTTestCase {
 
   private static native String getNodeName(Element elem) /*-{
-     return (elem.nodeName || "").toLowerCase();
-   }-*/;
+    return (elem.nodeName || "").toLowerCase();
+  }-*/;
 
   /**
    * Removes all elements in the body, except scripts and iframes.
@@ -85,6 +87,46 @@
     assertEquals(href, protocol + "//" + host + path + query + hash);
   }
 
+  public void testLocationCreateUrlBuilder() {
+    History.newItem("theHash");
+    String expected = Location.getHref();
+
+    // Build the string with the builder.
+    UrlBuilder builder = Location.createUrlBuilder();
+    String actual = builder.buildString();
+
+    // Check the hash.
+    {
+      String[] expectedParts = expected.split("#");
+      String[] actualParts = actual.split("#");
+      assertEquals(2, actualParts.length);
+      assertEquals(expectedParts[1], actualParts[1]);
+      expected = expectedParts[0];
+      actual = actualParts[0];
+    }
+
+    // Check the query parameters.
+    {
+      String[] expectedParts = expected.split("[?]");
+      String[] actualParts = actual.split("[?]");
+      if (expectedParts.length > 1) {
+        assertEquals(2, actualParts.length);
+        String[] expectedPairs = expectedParts[1].split("&");
+        String[] actualPairs = actualParts[1].split("&");
+        assertEquals(expectedPairs.length, actualPairs.length);
+        for (String actualPair : actualPairs) {
+          String[] kv = actualPair.split("=");
+          assertEquals(Location.getParameter(kv[0]), kv.length > 1 ? kv[1] : "");
+        }
+      }
+      expected = expectedParts[0];
+      actual = actualParts[0];
+    }
+
+    // Check everything but the query params and hash/
+    assertEquals(expected, actual);
+  }
+
   public void testLocationParsing() {
     Map<String, List<String>> map;
 
@@ -93,23 +135,22 @@
     assertEquals(map.size(), 3);
     assertEquals(map.get("foo").get(0), "bar");
     assertEquals(map.get("fuzzy").get(0), "bunnies");
-    
+
     // multiple values for the same parameter
     map = Window.Location.buildListParamMap(
         "?fuzzy=bunnies&foo=bar&num=42&foo=baz");
     assertEquals(map.size(), 3);
     assertEquals(map.get("foo").get(0), "bar");
     assertEquals(map.get("foo").get(1), "baz");
-    
+
     // no query parameters.
     map = Window.Location.buildListParamMap("");
     assertEquals(map.size(), 0);
-    
+
     // blank keys should be ignored, but blank values are OK. Also,
     // keys can contain whitespace. (but the browser may give whitespace
     // back as escaped).
-    map = Window.Location.buildListParamMap(
-        "?&& &a&b=&c=c&d=d=d&=e&f=2&f=1&");
+    map = Window.Location.buildListParamMap("?&& &a&b=&c=c&d=d=d&=e&f=2&f=1&");
     assertEquals(map.size(), 6);
     assertEquals(map.get(" ").get(0), "");
     assertEquals(map.get("a").get(0), "");
@@ -120,8 +161,7 @@
     assertEquals(map.get("f").get(1), "1");
 
     // Values escaped with hex codes should work too.
-    map = Window.Location.buildListParamMap(
-    "?foo=bar%20baz%3aqux");
+    map = Window.Location.buildListParamMap("?foo=bar%20baz%3aqux");
     assertEquals(map.get("foo").get(0), "bar baz:qux");
   }
 
@@ -190,7 +230,7 @@
   @SuppressWarnings("deprecation")
   static class ListenerTester implements WindowResizeListener {
     static int resize = 0;
- 
+
     public void onWindowResized(int width, int height) {
       ++resize;
     }
@@ -216,7 +256,7 @@
     Window.removeWindowResizeListener(r1);
     ListenerTester.fire();
     assertEquals(ListenerTester.resize, 1);
-    
+
     Window.removeWindowResizeListener(r2);
     ListenerTester.fire();
     assertEquals(ListenerTester.resize, 0);