HTML5 Storage API in GWT.

This change adds the HTML5 local and session storage APIs, and a Map interface
backed by storage. This is a contribution from an external project,
the gwt-mobile-webkit project, and is a copy of issue#1290802 with some additional changes.

Review at http://gwt-code-reviews.appspot.com/1374803

Review by: jlabanca@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9818 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/storage/Storage.gwt.xml b/user/src/com/google/gwt/storage/Storage.gwt.xml
new file mode 100644
index 0000000..47ee937
--- /dev/null
+++ b/user/src/com/google/gwt/storage/Storage.gwt.xml
@@ -0,0 +1,52 @@
+<!--                                                                        -->
+<!-- Copyright 2011 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   -->
+<!-- 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. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.core.Core" />
+  <inherits name="com.google.gwt.user.UserAgent" />
+
+  <!-- Define the storage support property -->
+  <define-property name="storageSupport" values="maybe,no" />
+
+  <!-- Set the default to maybe -->
+  <set-property name="storageSupport" value="maybe" />
+
+  <!-- Older browsers do not support Storage -->
+  <set-property name="storageSupport" value="no">
+    <any>
+      <when-property-is name="user.agent" value="ie6" />
+    </any>
+  </set-property>
+
+  <replace-with class="com.google.gwt.storage.client.Storage.StorageSupportDetectorNo">
+    <when-type-is class="com.google.gwt.storage.client.Storage.StorageSupportDetector" />
+    <when-property-is name="storageSupport" value="no" />
+  </replace-with>
+
+  <replace-with class="com.google.gwt.storage.client.StorageImplMozilla">
+    <when-type-is class="com.google.gwt.storage.client.StorageImpl" />
+    <when-property-is name="user.agent" value="gecko1_8" />
+  </replace-with>
+
+  <replace-with class="com.google.gwt.storage.client.StorageImplNonNativeEvents">
+    <when-type-is class="com.google.gwt.storage.client.StorageImpl" />
+    <when-property-is name="user.agent" value="safari" />
+  </replace-with>
+
+  <replace-with class="com.google.gwt.storage.client.StorageImplIE8">
+    <when-type-is class="com.google.gwt.storage.client.StorageImpl" />
+    <when-property-is name="user.agent" value="ie8" />
+  </replace-with>
+</module>
+
diff --git a/user/src/com/google/gwt/storage/client/Storage.java b/user/src/com/google/gwt/storage/client/Storage.java
new file mode 100644
index 0000000..4c5fd9e
--- /dev/null
+++ b/user/src/com/google/gwt/storage/client/Storage.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.PartialSupport;
+import com.google.gwt.event.shared.HandlerRegistration;
+
+/**
+ * Implements the HTML5 Storage interface.
+ *
+ * <p>
+ * You can obtain a Storage by either invoking
+ * {@link #getLocalStorageIfSupported()} or
+ * {@link #getSessionStorageIfSupported()}.
+ * </p>
+ *
+ * <p>
+ * <span style="color:red">Experimental API: This API is still under development
+ * and is subject to change. </span>
+ * </p>
+ *
+ * <p>
+ * If Web Storage is NOT supported in the browser, these methods return <code>
+ * null</code>.
+ * </p>
+ * 
+ * <p>
+ * Note: Storage events into other windows are not supported.
+ * </p>
+ *
+ * @see <a href="http://www.w3.org/TR/webstorage/#storage-0">W3C Web Storage -
+ *      Storage</a>
+ * @see <a
+ *      href="http://devworld.apple.com/safari/library/documentation/iPhone/Conceptual/SafariJSDatabaseGuide/Name-ValueStorage/Name-ValueStorage.html">Safari
+ *      Client-Side Storage and Offline Applications Programming Guide -
+ *      Key-Value Storage</a>
+ * @see <a href="http://quirksmode.org/dom/html5.html#t00">Quirksmode.org -
+ *      HTML5 Compatibility - Storage</a>
+ * @see <a
+ *      href="http://code.google.com/p/gwt-mobile-webkit/wiki/StorageApi">Wiki
+ *      - Quickstart Guide</a>
+ *
+ *      This may not be supported on all browsers.
+ */
+// TODO(pdr): Add support for Object values, instead of just Strings. The
+// Storage API spec specifies this, but browser support poor at the moment.
+// TODO(pdr): Add support for native events once browsers correctly implement
+// storage events.
+@PartialSupport
+public final class Storage {
+  /**
+   * Detector for browser support of Storage.
+   */
+  private static class StorageSupportDetector {
+    private final boolean isLocalStorageSupported = detectLocalStorageSupport();
+    private final boolean isSessionStorageSupported =
+        detectSessionStorageSupport();
+
+    public boolean isLocalStorageSupported() {
+      return isLocalStorageSupported;
+    }
+
+    public boolean isSessionStorageSupported() {
+      return isSessionStorageSupported;
+    }
+
+    private native boolean detectLocalStorageSupport() /*-{
+      return typeof $wnd.localStorage != "undefined";
+    }-*/;
+
+    private native boolean detectSessionStorageSupport() /*-{
+      return typeof $wnd.sessionStorage != "undefined";
+    }-*/;
+  }
+
+  /**
+   * Detector for browsers that do not support Storage.
+   */
+  @SuppressWarnings("unused")
+  private static class StorageSupportDetectorNo extends StorageSupportDetector {
+    @Override
+    public boolean isLocalStorageSupported() {
+      return false;
+    }
+
+    @Override
+    public boolean isSessionStorageSupported() {
+      return false;
+    }
+  }
+
+  static final StorageImpl impl = GWT.create(StorageImpl.class);
+
+  private static Storage localStorage;
+
+  private static Storage sessionStorage;
+
+  /**
+   * Singleton for Support detector.
+   */
+  private static StorageSupportDetector supportDetectorImpl;
+
+  /**
+   * Registers an event handler for StorageEvents.
+   *
+   * @see <a href="http://www.w3.org/TR/webstorage/#the-storage-event">W3C Web
+   *      Storage - the storage event</a>
+   * @param handler
+   * @return {@link HandlerRegistration} used to remove this handler
+   */
+  public static HandlerRegistration addStorageEventHandler(
+      StorageEvent.Handler handler) {
+    return impl.addStorageEventHandler(handler);
+  }
+
+  /**
+   * Returns a Local Storage.
+   *
+   * <p>
+   * The returned storage is associated with the <a
+   * href="http://www.w3.org/TR/html5/browsers.html#origin">origin</a> of the
+   * Document.
+   * </p>
+   *
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-localstorage">W3C Web
+   *      Storage - localStorage</a>
+   * @return the localStorage instance, or <code>null</code> if Web Storage is
+   *         NOT supported.
+   */
+  public static Storage getLocalStorageIfSupported() {
+    if (isLocalStorageSupported()) {
+      if (localStorage == null) {
+        localStorage = new Storage(StorageImpl.LOCAL_STORAGE);
+      }
+      return localStorage;
+    }
+    return null;
+  }
+
+  /**
+   * Returns a Session Storage.
+   *
+   * <p>
+   * The returned storage is associated with the current <a href=
+   * "http://www.w3.org/TR/html5/browsers.html#top-level-browsing-context"
+   * >top-level browsing context</a>.
+   * </p>
+   *
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-sessionstorage">W3C Web
+   *      Storage - sessionStorage</a>
+   * @return the sessionStorage instance, or <code>null</code> if Web Storage is
+   *         NOT supported.
+   */
+  public static Storage getSessionStorageIfSupported() {
+    if (isSessionStorageSupported()) {
+      if (sessionStorage == null) {
+        sessionStorage = new Storage(StorageImpl.SESSION_STORAGE);
+      }
+      return sessionStorage;
+    }
+    return null;
+  }
+
+  /**
+   * Returns <code>true</code> if the <code>localStorage</code> part of the
+   * Storage API is supported on the running platform.
+   */
+  public static boolean isLocalStorageSupported() {
+    return getStorageSupportDetector().isLocalStorageSupported();
+  }
+
+  /**
+   * Returns <code>true</code> if the <code>sessionStorage</code> part of the
+   * Storage API is supported on the running platform.
+   */
+  public static boolean isSessionStorageSupported() {
+    return getStorageSupportDetector().isSessionStorageSupported();
+  }
+
+  /**
+   * Returns <code>true</code> if the Storage API (both localStorage and
+   * sessionStorage) is supported on the running platform.
+   */
+  public static boolean isSupported() {
+    return isLocalStorageSupported() && isSessionStorageSupported();
+  }
+
+  /**
+   * De-registers an event handler for StorageEvents.
+   *
+   * @see <a href="http://www.w3.org/TR/webstorage/#the-storage-event">W3C Web
+   *      Storage - the storage event</a>
+   * @param handler
+   */
+  public static void removeStorageEventHandler(StorageEvent.Handler handler) {
+    impl.removeStorageEventHandler(handler);
+  }
+
+  private static StorageSupportDetector getStorageSupportDetector() {
+    if (supportDetectorImpl == null) {
+      supportDetectorImpl = GWT.create(StorageSupportDetector.class);
+    }
+    return supportDetectorImpl;
+  }
+
+  // Contains either "localStorage" or "sessionStorage":
+  private final String storage;
+
+  /**
+   * This class can never be instantiated externally. Use
+   * {@link #getLocalStorageIfSupported()} or
+   * {@link #getSessionStorageIfSupported()} instead.
+   */
+  private Storage(String storage) {
+    this.storage = storage;
+  }
+
+  /**
+   * Removes all items in the Storage.
+   *
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-clear">W3C Web
+   *      Storage - Storage.clear()</a>
+   */
+  public void clear() {
+    impl.clear(storage);
+  }
+
+  /**
+   * Returns the item in the Storage associated with the specified key.
+   *
+   * @param key the key to a value in the Storage
+   * @return the value associated with the given key
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-getitem">W3C Web
+   *      Storage - Storage.getItem(k)</a>
+   */
+  public String getItem(String key) {
+    return impl.getItem(storage, key);
+  }
+
+  /**
+   * Returns the number of items in this Storage.
+   *
+   * @return number of items in this Storage
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-l">W3C Web
+   *      Storage - Storage.length()</a>
+   */
+  public int getLength() {
+    return impl.getLength(storage);
+  }
+
+  /**
+   * Returns the key at the specified index.
+   *
+   * @param index the index of the key
+   * @return the key at the specified index in this Storage
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-key">W3C Web
+   *      Storage - Storage.key(n)</a>
+   */
+  public String key(int index) {
+    return impl.key(storage, index);
+  }
+
+  /**
+   * Removes the item in the Storage associated with the specified key.
+   *
+   * @param key the key to a value in the Storage
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-removeitem">W3C
+   *      Web Storage - Storage.removeItem(k)</a>
+   */
+  public void removeItem(String key) {
+    impl.removeItem(storage, key);
+  }
+
+  /**
+   * Sets the value in the Storage associated with the specified key to the
+   * specified data.
+   *
+   * Note: The empty string may not be used as a key.
+   *
+   * @param key the key to a value in the Storage
+   * @param data the value associated with the key
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-setitem">W3C Web
+   *      Storage - Storage.setItem(k,v)</a>
+   */
+  public void setItem(String key, String data) {
+    // prevent the empty string due to a Firefox bug:
+    // bugzilla.mozilla.org/show_bug.cgi?id=510849
+    assert key.length() > 0;
+    impl.setItem(storage, key, data);
+  }
+}
diff --git a/user/src/com/google/gwt/storage/client/StorageEvent.java b/user/src/com/google/gwt/storage/client/StorageEvent.java
new file mode 100644
index 0000000..6f01b56
--- /dev/null
+++ b/user/src/com/google/gwt/storage/client/StorageEvent.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Represents a Storage Event.
+ * 
+ * <p>
+ * <span style="color:red">Experimental API: This API is still under development
+ * and is subject to change. </span>
+ * </p>
+ * 
+ * <p>
+ * A Storage Event is fired when a storage area changes, as described in these
+ * two sections (for <a
+ * href="http://www.w3.org/TR/webstorage/#sessionStorageEvent">session
+ * storage</a>, for <a
+ * href="http://www.w3.org/TR/webstorage/#localStorageEvent">local storage</a>).
+ * </p>
+ * 
+ * @see Handler
+ * @see <a href="http://www.w3.org/TR/webstorage/#event-definition">W3C Web
+ *      Storage - StorageEvent</a>
+ * @see <a
+ *      href="https://developer.apple.com/safari/library/documentation/AppleApplications/Reference/WebKitDOMRef/StorageEvent_idl/Classes/StorageEvent/index.html">Safari
+ *      StorageEvent reference</a>
+ */
+public final class StorageEvent extends JavaScriptObject {
+  /**
+   * Represents an Event handler for {@link StorageEvent}s.
+   * 
+   * <p>
+   * Apply your StorageEventHandler using
+   * {@link Storage#addStorageEventHandler(Handler)}.
+   * </p>
+   * 
+   * @see StorageEvent
+   */
+  public interface Handler {
+    /**
+     * Invoked when a StorageEvent is fired.
+     * 
+     * @param event the fired StorageEvent
+     * @see <a href="http://www.w3.org/TR/webstorage/#event-storage">W3C Web
+     *      Storage - Storage Event</a>
+     */
+    void onStorageChange(StorageEvent event);
+  }
+
+  protected StorageEvent() {
+  }
+
+  /**
+   * Returns the key being changed.
+   * 
+   * @return the key being changed
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storageevent-key">W3C
+   *      Web Storage - StorageEvent.key</a>
+   */
+  public native String getKey() /*-{
+    return this.key;
+  }-*/;
+
+  /**
+   * Returns the new value of the key being changed.
+   * 
+   * @return the new value of the key being changed
+   * @see <a
+   *      href="http://www.w3.org/TR/webstorage/#dom-storageevent-newvalue">W3C
+   *      Web Storage - StorageEvent.newValue</a>
+   */
+  public native String getNewValue() /*-{
+    return this.newValue;
+  }-*/;
+
+  /**
+   * Returns the old value of the key being changed.
+   * 
+   * @return the old value of the key being changed
+   * @see <a
+   *      href="http://www.w3.org/TR/webstorage/#dom-storageevent-oldvalue">W3C
+   *      Web Storage - StorageEvent.oldValue</a>
+   */
+  public native String getOldValue() /*-{
+    return this.oldValue;
+  }-*/;
+
+  /**
+   * Returns the {@link Storage} object that was affected.
+   * 
+   * @return the {@link Storage} object that was affected
+   * @see <a
+   *      href="http://www.w3.org/TR/webstorage/#dom-storageevent-storagearea">W3C
+   *      Web Storage - StorageEvent.storageArea</a>
+   */
+  public Storage getStorageArea() {
+    return Storage.impl.getStorageFromEvent(this);
+  }
+
+  /**
+   * Returns the address of the document whose key changed.
+   * 
+   * @return the address of the document whose key changed
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storageevent-url">W3C
+   *      Web Storage - StorageEvent.url</a>
+   */
+  public native String getUrl() /*-{
+    return this.url || this.uri;  // Older Safari browsers have 'uri' instead of 'url'
+  }-*/;
+}
+
diff --git a/user/src/com/google/gwt/storage/client/StorageImpl.java b/user/src/com/google/gwt/storage/client/StorageImpl.java
new file mode 100644
index 0000000..f943939
--- /dev/null
+++ b/user/src/com/google/gwt/storage/client/StorageImpl.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.event.shared.HandlerRegistration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is the HTML5 Storage implementation according to the <a
+ * href="http://www.w3.org/TR/webstorage/#storage-0">standard
+ * recommendation</a>.
+ *
+ * <p>
+ * Never use this class directly, instead use {@link Storage}.
+ * </p>
+ *
+ * @see <a href="http://www.w3.org/TR/webstorage/#storage-0">W3C Web Storage -
+ *      Storage</a>
+ */
+class StorageImpl {
+
+  public static final String LOCAL_STORAGE = "localStorage";
+  public static final String SESSION_STORAGE = "sessionStorage";
+
+  protected static List<StorageEvent.Handler> storageEventHandlers;
+  protected static JavaScriptObject jsHandler;
+
+  /**
+   * Handles StorageEvents if a {@link StorageEvent.Handler} is registered.
+   */
+  protected static final void handleStorageEvent(StorageEvent event) {
+    if (!hasStorageEventHandlers()) {
+      return;
+    }
+    UncaughtExceptionHandler ueh = GWT.getUncaughtExceptionHandler();
+    for (StorageEvent.Handler handler : storageEventHandlers) {
+      if (ueh != null) {
+        try {
+          handler.onStorageChange(event);
+        } catch (Throwable t) {
+          ueh.onUncaughtException(t);
+        }
+      } else {
+        handler.onStorageChange(event);
+      }
+    }
+  }
+
+  /**
+   * Returns <code>true</code> if at least one StorageEvent handler is
+   * registered, <code>false</code> otherwise.
+   */
+  protected static boolean hasStorageEventHandlers() {
+    return storageEventHandlers != null && !storageEventHandlers.isEmpty();
+  }
+
+  /**
+   * This class can never be instantiated by itself.
+   */
+  protected StorageImpl() {
+  }
+
+  /**
+   * Registers an event handler for StorageEvents.
+   *
+   * @see <a href="http://www.w3.org/TR/webstorage/#the-storage-event">W3C Web
+   *      Storage - the storage event</a>
+   * @param handler
+   * @return {@link HandlerRegistration} used to remove this handler
+   */
+  public HandlerRegistration addStorageEventHandler(
+      final StorageEvent.Handler handler) {
+    getStorageEventHandlers().add(handler);
+    if (storageEventHandlers.size() == 1) {
+      addStorageEventHandler0();
+    }
+    return new HandlerRegistration() {
+      public void removeHandler() {
+        StorageImpl.this.removeStorageEventHandler(handler);
+      }
+    };
+  }
+
+  /**
+   * Removes all items in the Storage.
+   *
+   * @param storage either {@link #LOCAL_STORAGE} or {@link #SESSION_STORAGE}
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-clear">W3C Web
+   *      Storage - Storage.clear()</a>
+   */
+  public native void clear(String storage) /*-{
+    $wnd[storage].clear();
+  }-*/;
+
+  /**
+   * Returns the item in the Storage associated with the specified key.
+   *
+   * @param storage either {@link #LOCAL_STORAGE} or {@link #SESSION_STORAGE}
+   * @param key the key to a value in the Storage
+   * @return the value associated with the given key
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-getitem">W3C Web
+   *      Storage - Storage.getItem(k)</a>
+   */
+  public native String getItem(String storage, String key) /*-{
+    return $wnd[storage].getItem(key);
+  }-*/;
+
+  /**
+   * Returns the number of items in this Storage.
+   *
+   * @param storage either {@link #LOCAL_STORAGE} or {@link #SESSION_STORAGE}
+   * @return number of items in this Storage
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-l">W3C Web
+   *      Storage - Storage.length()</a>
+   */
+  public native int getLength(String storage) /*-{
+    return $wnd[storage].length;
+  }-*/;
+
+  /**
+   * Returns the key at the specified index.
+   *
+   * @param storage either {@link #LOCAL_STORAGE} or {@link #SESSION_STORAGE}
+   * @param index the index of the key
+   * @return the key at the specified index in this Storage
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-key">W3C Web
+   *      Storage - Storage.key(n)</a>
+   */
+  public native String key(String storage, int index) /*-{
+    return $wnd[storage].key(index);
+  }-*/;
+
+  /**
+   * Removes the item in the Storage associated with the specified key.
+   *
+   * @param storage either {@link #LOCAL_STORAGE} or {@link #SESSION_STORAGE}
+   * @param key the key to a value in the Storage
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-removeitem">W3C
+   *      Web Storage - Storage.removeItem(k)</a>
+   */
+  public native void removeItem(String storage, String key) /*-{
+    $wnd[storage].removeItem(key);
+  }-*/;
+
+  /**
+   * De-registers an event handler for StorageEvents.
+   *
+   * @see <a href="http://www.w3.org/TR/webstorage/#the-storage-event">W3C Web
+   *      Storage - the storage event</a>
+   * @param handler
+   */
+  public void removeStorageEventHandler(StorageEvent.Handler handler) {
+    getStorageEventHandlers().remove(handler);
+    if (storageEventHandlers.isEmpty()) {
+      removeStorageEventHandler0();
+    }
+  }
+
+  /**
+   * Sets the value in the Storage associated with the specified key to the
+   * specified data.
+   *
+   * @param storage either {@link #LOCAL_STORAGE} or {@link #SESSION_STORAGE}
+   * @param key the key to a value in the Storage
+   * @param data the value associated with the key
+   * @see <a href="http://www.w3.org/TR/webstorage/#dom-storage-setitem">W3C Web
+   *      Storage - Storage.setItem(k,v)</a>
+   */
+  public native void setItem(String storage, String key, String data) /*-{
+    $wnd[storage].setItem(key, data);
+  }-*/;
+
+  protected native void addStorageEventHandler0() /*-{
+    @com.google.gwt.storage.client.StorageImpl::jsHandler = $entry(function(event) {
+      @com.google.gwt.storage.client.StorageImpl::handleStorageEvent(Lcom/google/gwt/storage/client/StorageEvent;)(event);
+    });
+    $wnd.addEventListener("storage", 
+      @com.google.gwt.storage.client.StorageImpl::jsHandler, false);
+  }-*/;
+
+  /**
+   * Returns the {@link List} of {@link StorageEvent.Handler}s 
+   * registered, which is never <code>null</code>.
+   */
+  protected List<StorageEvent.Handler> getStorageEventHandlers() {
+    if (storageEventHandlers == null) {
+      storageEventHandlers = new ArrayList<StorageEvent.Handler>();
+    }
+    return storageEventHandlers;
+  }
+
+  /**
+   * Returns the {@link Storage} object that was affected in the event.
+   * 
+   * @return the {@link Storage} object that was affected in the event.
+   */
+  protected native Storage getStorageFromEvent(StorageEvent event) /*-{
+    if (event.storageArea === $wnd["localStorage"]) {
+      return @com.google.gwt.storage.client.Storage::getLocalStorageIfSupported()();
+    } else {
+      return @com.google.gwt.storage.client.Storage::getSessionStorageIfSupported()();
+    }
+  }-*/;
+
+  protected native void removeStorageEventHandler0() /*-{
+    $wnd.removeEventListener("storage", 
+      @com.google.gwt.storage.client.StorageImpl::jsHandler, false);
+  }-*/;
+}
diff --git a/user/src/com/google/gwt/storage/client/StorageImplIE8.java b/user/src/com/google/gwt/storage/client/StorageImplIE8.java
new file mode 100644
index 0000000..358fd09
--- /dev/null
+++ b/user/src/com/google/gwt/storage/client/StorageImplIE8.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+/**
+ * IE8-specific implementation of the Web Storage.
+ *
+ * @see <a
+ *      href="http://msdn.microsoft.com/en-us/library/cc197062(VS.85).aspx">MSDN
+ *      - Introduction to DOM Storage</a>
+ */
+class StorageImplIE8 extends StorageImplNonNativeEvents {
+  /*
+   * IE8 will throw "JavaScriptException: (Error): Invalid argument." for
+   * indices outside the range of 0 - storage.length(). In this impl method, we
+   * return null instead, in order to match the Storage spec.
+   */
+  @Override
+  public native String key(String storage, int index) /*-{
+    return (index >= 0 && index < $wnd[storage].length) ? 
+      $wnd[storage].key(index) : null;
+  }-*/;
+
+  /*
+   * IE8 will throw "Class doesn't support Automation" error when comparing
+   * $wnd["localStorage"] === $wnd["localStorage"]. In this impl method, we
+   * work around it by using an attribute on the StorageEvent.
+   */
+  @Override
+  protected native Storage getStorageFromEvent(StorageEvent event) /*-{
+    if (event.storage == "localStorage") {
+      return @com.google.gwt.storage.client.Storage::getLocalStorageIfSupported()();
+    } else {
+      return @com.google.gwt.storage.client.Storage::getSessionStorageIfSupported()();
+    }
+  }-*/;
+}
diff --git a/user/src/com/google/gwt/storage/client/StorageImplMozilla.java b/user/src/com/google/gwt/storage/client/StorageImplMozilla.java
new file mode 100644
index 0000000..00e2b38
--- /dev/null
+++ b/user/src/com/google/gwt/storage/client/StorageImplMozilla.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+
+/**
+ * Mozilla-specific implementation of a Storage.
+ *
+ * <p>
+ * Implementation of StorageEvents is incomplete for Mozilla. This class amends
+ * the properties consistently with W3C's StorageEvent.
+ * </p>
+ */
+class StorageImplMozilla extends StorageImplNonNativeEvents {
+  /*
+   * Firefox incorrectly handles indices outside the range of 
+   * 0 to storage.length(). See bugzilla.mozilla.org/show_bug.cgi?id=50924
+   */
+  @Override
+  public native String key(String storage, int index) /*-{
+    return (index >= 0 && index < $wnd[storage].length) ? 
+      $wnd[storage].key(index) : null;
+  }-*/;
+}
diff --git a/user/src/com/google/gwt/storage/client/StorageImplNonNativeEvents.java b/user/src/com/google/gwt/storage/client/StorageImplNonNativeEvents.java
new file mode 100644
index 0000000..97dcf47
--- /dev/null
+++ b/user/src/com/google/gwt/storage/client/StorageImplNonNativeEvents.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+/**
+ * Implementation of Storage with non-native events.
+ *
+ * <p>
+ * Implementation of StorageEvents is incomplete for many browsers. This class 
+ * amends the properties consistently with W3C's StorageEvent.
+ * </p>
+ */
+class StorageImplNonNativeEvents extends StorageImpl {
+  private static native StorageEvent createStorageEvent(
+      String key, String oldValue, String newValue, String storage) /*-{
+    return {
+      key: key,
+      oldValue: oldValue,
+      newValue: newValue,
+      storage: storage,
+      storageArea: $wnd[storage],
+      url: $wnd.location.href
+    };
+  }-*/;
+
+  @SuppressWarnings("unused")
+  private static void fireStorageEvent(
+      String key, String oldValue, String newValue, String storage) {
+    if (hasStorageEventHandlers()) {
+      StorageEvent se = createStorageEvent(key, oldValue, newValue, storage);
+      handleStorageEvent(se);
+    }
+  }
+
+  public void clear(String storage) {
+    super.clear(storage);
+    fireStorageEvent(null, null, null, storage);
+  }
+
+  @Override
+  public void removeItem(String storage, String key) {
+    String oldValue = getItem(storage, key);
+    super.removeItem(storage, key);
+    fireStorageEvent(key, oldValue, null, storage);
+  }
+
+  @Override
+  public void setItem(String storage, String key, String data) {
+    String oldValue = getItem(storage, key);
+    super.setItem(storage, key, data);
+    fireStorageEvent(key, oldValue, data, storage);
+  }
+
+  @Override
+  protected void addStorageEventHandler0() {
+    // no-op
+  }
+
+  @Override
+  protected void removeStorageEventHandler0() {
+    // no-op
+  }
+}
diff --git a/user/src/com/google/gwt/storage/client/StorageMap.java b/user/src/com/google/gwt/storage/client/StorageMap.java
new file mode 100644
index 0000000..6a4c892
--- /dev/null
+++ b/user/src/com/google/gwt/storage/client/StorageMap.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * Exposes the local/session {@link Storage} as a standard {@link Map
+ * Map&lt;String, String&gt;}.
+ *
+ * <p>
+ * <span style="color:red">Experimental API: This API is still under development
+ * and is subject to change. </span>
+ * </p>
+ *
+ * <p>
+ * The following characteristics are associated with this Map:
+ * </p>
+ * <ol>
+ * <li><em>Mutable</em> - All 'write' methods ({@link #put(String, String)},
+ * {@link #putAll(Map)}, {@link #remove(Object)}, {@link #clear()},
+ * {@link Entry#setValue(Object)}) operate as intended;</li>
+ * <li><em>remove() on Iterators</em> - All remove() operations on available
+ * Iterators (from {@link #keySet()}, {@link #entrySet()} and {@link #values()})
+ * operate as intended;</li>
+ * <li><em>No <code>null</code> values and keys</em> - The Storage doesn't
+ * accept keys or values which are <code>null</code>;</li>
+ * <li><em>JavaScriptException instead of NullPointerException</em> - Some Map
+ * (or other Collection) methods mandate the use of a
+ * {@link NullPointerException} if some argument is <code>null</code> (e.g.
+ * {@link #remove(Object)} remove(null)). this Map emits
+ * {@link JavaScriptException}s instead;</li>
+ * <li><em>String values and keys</em> - All keys and values in this Map are
+ * String types.</li>
+ * </ol>
+ */
+public class StorageMap extends AbstractMap<String, String> {
+
+  /*
+   * Represents a Map.Entry to a Storage item
+   */
+  private class StorageEntry implements Map.Entry<String, String> {
+    private final String key;
+
+    public StorageEntry(String key) {
+      this.key = key;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public boolean equals(Object obj) {
+      if (obj == null) {
+        return false;
+      } else if (obj == this) {
+        return true;
+      } else if (!(obj instanceof Map.Entry)) {
+        return false;
+      }
+
+      Map.Entry e = (Map.Entry) obj;
+
+      return eq(key, e.getKey()) && eq(getValue(), e.getValue());
+    }
+
+    public String getKey() {
+      return key;
+    }
+
+    public String getValue() {
+      return storage.getItem(key);
+    }
+
+    @Override
+    public int hashCode() {
+      String value = getValue();
+      return (key == null ? 0 : key.hashCode())
+          ^ (value == null ? 0 : value.hashCode());
+    }
+
+    public String setValue(String value) {
+      String oldValue = storage.getItem(key);
+      storage.setItem(key, value);
+      return oldValue;
+    }
+  }
+
+  /*
+   * Represents an Iterator over all Storage items
+   */
+  private class StorageEntryIterator implements Iterator<
+      Map.Entry<String, String>> {
+    private int index = -1;
+    private boolean removed = false;
+
+    public boolean hasNext() {
+      return index < size() - 1;
+    }
+
+    public Map.Entry<String, String> next() {
+      if (hasNext()) {
+        index++;
+        removed = false;
+        return new StorageEntry(storage.key(index));
+      }
+      throw new NoSuchElementException();
+    }
+
+    public void remove() {
+      if (index >= 0 && index < size()) {
+        if (removed) {
+          throw new IllegalStateException(
+              "Cannot remove() Entry - already removed!");
+        }
+        storage.removeItem(storage.key(index));
+        removed = true;
+        index--;
+      } else {
+        throw new IllegalStateException(
+            "Cannot remove() Entry - index=" + index + ", size=" + size());
+      }
+    }
+  }
+
+  /*
+   * Represents a Set<Map.Entry> over all Storage items
+   */
+  private class StorageEntrySet extends AbstractSet<Map.Entry<String, String>> {
+    public void clear() {
+      StorageMap.this.clear();
+    }
+
+    @SuppressWarnings("unchecked")
+    public boolean contains(Object o) {
+      if (o == null || !(o instanceof Map.Entry)) {
+        return false;
+      }
+      Map.Entry e = (Map.Entry) o;
+      Object key = e.getKey();
+      return key != null && containsKey(key) && eq(get(key), e.getValue());
+    }
+
+    public Iterator<Map.Entry<String, String>> iterator() {
+      return new StorageEntryIterator();
+    }
+
+    @SuppressWarnings("unchecked")
+    public boolean remove(Object o) {
+      if (o == null || !(o instanceof Map.Entry)) {
+        return false;
+      }
+      Map.Entry e = (Map.Entry) o;
+      if (e.getKey() == null) {
+        return false;
+      }
+      String key = e.getKey().toString();
+      String value = storage.getItem(key);
+      if (eq(value, e.getValue())) {
+        return StorageMap.this.remove(key) != null;
+      }
+      return false;
+    }
+
+    public int size() {
+      return StorageMap.this.size();
+    }
+  }
+
+  private Storage storage;
+  private StorageEntrySet entrySet;
+
+  /**
+   * Creates the Map with the specified Storage as data provider.
+   *
+   * @param storage a local/session Storage instance obtained by either
+   *          {@link Storage#getLocalStorageIfSupported()} or
+   *          {@link Storage#getSessionStorageIfSupported()}.
+   */
+  public StorageMap(Storage storage) {
+    assert storage != null : "storage cannot be null";
+    this.storage = storage;
+  }
+
+  /**
+   * Removes all items from the Storage.
+   *
+   * @see Storage#clear()
+   */
+  public void clear() {
+    storage.clear();
+  }
+
+  /**
+   * Returns <code>true</code> if the Storage contains the specified key, <code>
+   * false</code> otherwise.
+   */
+  public boolean containsKey(Object key) {
+    return storage.getItem(key.toString()) != null;
+  }
+
+  /**
+   * Returns <code>true</code> if the Storage contains the specified value,
+   * <code>false</code> otherwise (or if the specified key is <code>null</code>
+   * ).
+   */
+  public boolean containsValue(Object value) {
+    int s = size();
+    for (int i = 0; i < s; i++) {
+      if (value.equals(storage.getItem(storage.key(i)))) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns a Set containing all entries of the Storage.
+   */
+  public Set<Map.Entry<String, String>> entrySet() {
+    if (entrySet == null) {
+      entrySet = new StorageEntrySet();
+    }
+    return entrySet;
+  }
+
+  /**
+   * Returns the value associated with the specified key in the Storage.
+   *
+   * @param key the key identifying the value
+   * @see Storage#getItem(String)
+   */
+  public String get(Object key) {
+    if (key == null) {
+      return null;
+    }
+    return storage.getItem(key.toString());
+  }
+
+  /**
+   * Adds (or overwrites) a new key/value pair in the Storage.
+   *
+   * @param key the key identifying the value (not <code>null</code>)
+   * @param value the value associated with the key (not <code>null</code>)
+   * @see Storage#setItem(String, String)
+   */
+  public String put(String key, String value) {
+    if (key == null || value == null) {
+      throw new IllegalArgumentException("Key and value cannot be null!");
+    }
+    String old = storage.getItem(key);
+    storage.setItem(key, value);
+    return old;
+  }
+
+  /**
+   * Removes the key/value pair from the Storage.
+   *
+   * @param key the key identifying the item to remove
+   * @return the value associated with the key - <code>null</code> if the key
+   *         was not present in the Storage
+   * @see Storage#removeItem(String)
+   */
+  public String remove(Object key) {
+    String k = key.toString();
+    String old = storage.getItem(k);
+    storage.removeItem(k);
+    return old;
+  }
+
+  /**
+   * Returns the number of items in the Storage.
+   *
+   * @return the number of items
+   * @see Storage#getLength()
+   */
+  public int size() {
+    return storage.getLength();
+  }
+
+  private boolean eq(Object a, Object b) {
+    if (a == b) {
+      return true;
+    }
+    if (a == null) {
+      return false;
+    }
+    return a.equals(b);
+  }
+}
diff --git a/user/src/com/google/gwt/storage/client/package.html b/user/src/com/google/gwt/storage/client/package.html
new file mode 100644
index 0000000..3a55523
--- /dev/null
+++ b/user/src/com/google/gwt/storage/client/package.html
@@ -0,0 +1,37 @@
+<html>
+<head>
+<!-- Copyright 2011 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   -->
+<!-- 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. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+</head>
+<body>
+
+Provides for key-value Storage services.
+
+
+<h2>Package Specification</h2>
+
+This package contains the public interface to the Storage API. All
+Storage services are to be accessed using types from this package,
+starting with {@link com.google.gwt.storage.client.Storage}.
+
+<h2>Related Documentation</h2>
+
+For tutorials, examples, guides, and background documentation, please see:
+<ul>
+  <li><a href="http://code.google.com/p/gwt-mobile-webkit/w/list?q=label:API-Storage">Wiki - GWT Mobile WebKit</a></li>
+  <li><a href="http://www.w3.org/TR/webstorage/">Web Storage Specification - W3C</a></li>
+</ul>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/user/src/com/google/gwt/user/User.gwt.xml b/user/src/com/google/gwt/user/User.gwt.xml
index 5611bc9..f8b77ee 100644
--- a/user/src/com/google/gwt/user/User.gwt.xml
+++ b/user/src/com/google/gwt/user/User.gwt.xml
@@ -55,6 +55,7 @@
    <inherits name="com.google.gwt.user.datepicker.DatePicker"/>
    <inherits name="com.google.gwt.user.cellview.CellView"/>
    <inherits name="com.google.gwt.safehtml.SafeHtml" />
+   <inherits name="com.google.gwt.storage.Storage" />
 
    <super-source path="translatable"/>
    <source path="client"/>
diff --git a/user/test/com/google/gwt/storage/StorageMapSuite.java b/user/test/com/google/gwt/storage/StorageMapSuite.java
new file mode 100644
index 0000000..77a6dda
--- /dev/null
+++ b/user/test/com/google/gwt/storage/StorageMapSuite.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2011 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.storage;
+
+import com.google.gwt.junit.tools.GWTTestSuite;
+import com.google.gwt.storage.client.LocalStorageMapTest;
+import com.google.gwt.storage.client.SessionStorageMapTest;
+
+import junit.framework.Test;
+
+/**
+ * Suite for all Storage Map tests.
+ */
+public class StorageMapSuite {
+  public static Test suite() {
+    GWTTestSuite suite = new GWTTestSuite("Storage Map Tests");
+
+    suite.addTestSuite(LocalStorageMapTest.class);
+    suite.addTestSuite(SessionStorageMapTest.class);
+
+    return suite;
+  }
+}
diff --git a/user/test/com/google/gwt/storage/StorageSuite.java b/user/test/com/google/gwt/storage/StorageSuite.java
new file mode 100644
index 0000000..78d7195
--- /dev/null
+++ b/user/test/com/google/gwt/storage/StorageSuite.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2011 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.storage;
+
+import com.google.gwt.junit.tools.GWTTestSuite;
+import com.google.gwt.storage.client.LocalStorageTest;
+import com.google.gwt.storage.client.SessionStorageTest;
+
+import junit.framework.Test;
+
+/**
+ * Suite for all Storage tests.
+ */
+public class StorageSuite {
+  public static Test suite() {
+    GWTTestSuite suite = new GWTTestSuite("Storage Tests");
+
+    suite.addTestSuite(LocalStorageTest.class);
+    suite.addTestSuite(SessionStorageTest.class);
+
+    return suite;
+  }
+}
diff --git a/user/test/com/google/gwt/storage/client/LocalStorageMapTest.java b/user/test/com/google/gwt/storage/client/LocalStorageMapTest.java
new file mode 100644
index 0000000..0844322
--- /dev/null
+++ b/user/test/com/google/gwt/storage/client/LocalStorageMapTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests Local {@link StorageMap}.
+ * 
+ * Because HtmlUnit does not support Storage, you will need to run these tests
+ * manually by adding this line to your VM args: -Dgwt.args="-runStyle Manual:1"
+ * If you are using Eclipse and GPE, go to "run configurations" or
+ * "debug configurations", select the test you would like to run, and put this
+ * line in the VM args under the arguments tab: -Dgwt.args="-runStyle Manual:1"
+ */
+@DoNotRunWith(Platform.HtmlUnitUnknown)
+public class LocalStorageMapTest extends StorageMapTest {
+  @Override
+  Storage getStorage() {
+    return Storage.getLocalStorageIfSupported();
+  }
+}
diff --git a/user/test/com/google/gwt/storage/client/LocalStorageTest.java b/user/test/com/google/gwt/storage/client/LocalStorageTest.java
new file mode 100644
index 0000000..ce93efb
--- /dev/null
+++ b/user/test/com/google/gwt/storage/client/LocalStorageTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests Local {@link Storage}.
+ * 
+ * Because HtmlUnit does not support Storage, you will need to run these tests
+ * manually by adding this line to your VM args: -Dgwt.args="-runStyle Manual:1"
+ * If you are using Eclipse and GPE, go to "run configurations" or
+ * "debug configurations", select the test you would like to run, and put this
+ * line in the VM args under the arguments tab: -Dgwt.args="-runStyle Manual:1"
+ */
+@DoNotRunWith(Platform.HtmlUnitUnknown)
+public class LocalStorageTest extends StorageTest {
+  @Override
+  Storage getStorage() {
+    return Storage.getLocalStorageIfSupported();
+  }
+}
diff --git a/user/test/com/google/gwt/storage/client/MapInterfaceTest.java b/user/test/com/google/gwt/storage/client/MapInterfaceTest.java
new file mode 100644
index 0000000..a5cab5b
--- /dev/null
+++ b/user/test/com/google/gwt/storage/client/MapInterfaceTest.java
@@ -0,0 +1,1583 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.core.client.JavaScriptException;
+import com.google.gwt.junit.client.GWTTestCase;
+
+import static java.util.Collections.singleton;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * Tests representing the contract of {@link Map}. Concrete subclasses of this
+ * base class test conformance of concrete {@link Map} subclasses to that
+ * contract.
+ * 
+ * TODO: Descriptive assertion messages, with hints as to probable fixes. 
+ * TODO: Add another constructor parameter indicating whether the class under 
+ * test is ordered, and check the order if so.
+ * TODO: Refactor to share code with SetTestBuilder.
+ * 
+ * @param <K> the type of keys used by the maps under test
+ * @param <V> the type of mapped values used the maps under test
+ */
+public abstract class MapInterfaceTest<K, V> extends GWTTestCase {
+
+  protected final boolean supportsPut;
+  protected final boolean supportsRemove;
+  protected final boolean supportsClear;
+  protected final boolean allowsNullKeys;
+  protected final boolean allowsNullValues;
+  protected final boolean supportsIteratorRemove;
+
+  /**
+   * Creates a new, empty instance of the class under test.
+   * 
+   * @return a new, empty map instance.
+   * @throws UnsupportedOperationException if it's not possible to make an empty
+   *           instance of the class under test.
+   */
+  protected abstract Map<K, V> makeEmptyMap()
+      throws UnsupportedOperationException;
+
+  /**
+   * Creates a new, non-empty instance of the class under test.
+   * 
+   * @return a new, non-empty map instance.
+   * @throws UnsupportedOperationException if it's not possible to make a
+   *           non-empty instance of the class under test.
+   */
+  protected abstract Map<K, V> makePopulatedMap()
+      throws UnsupportedOperationException;
+
+  /**
+   * Creates a new key that is not expected to be found in
+   * {@link #makePopulatedMap()}.
+   * 
+   * @return a key.
+   * @throws UnsupportedOperationException if it's not possible to make a key
+   *           that will not be found in the map.
+   */
+  protected abstract K getKeyNotInPopulatedMap()
+      throws UnsupportedOperationException;
+
+  /**
+   * Creates a new value that is not expected to be found in
+   * {@link #makePopulatedMap()}.
+   * 
+   * @return a value.
+   * @throws UnsupportedOperationException if it's not possible to make a value
+   *           that will not be found in the map.
+   */
+  protected abstract V getValueNotInPopulatedMap()
+      throws UnsupportedOperationException;
+
+  /**
+   * Constructor that assigns {@code supportsIteratorRemove} the same value as
+   * {@code supportsRemove}.
+   */
+  protected MapInterfaceTest(boolean allowsNullKeys, boolean allowsNullValues,
+      boolean supportsPut, boolean supportsRemove, boolean supportsClear) {
+    this(allowsNullKeys, allowsNullValues, supportsPut, supportsRemove,
+        supportsClear, supportsRemove);
+  }
+
+  /**
+   * Constructor with an explicit {@code supportsIteratorRemove} parameter.
+   */
+  protected MapInterfaceTest(boolean allowsNullKeys, boolean allowsNullValues,
+      boolean supportsPut, boolean supportsRemove, boolean supportsClear,
+      boolean supportsIteratorRemove) {
+    this.supportsPut = supportsPut;
+    this.supportsRemove = supportsRemove;
+    this.supportsClear = supportsClear;
+    this.allowsNullKeys = allowsNullKeys;
+    this.allowsNullValues = allowsNullValues;
+    this.supportsIteratorRemove = supportsIteratorRemove;
+  }
+
+  /**
+   * Used by tests that require a map, but don't care whether it's populated or
+   * not.
+   * 
+   * @return a new map instance.
+   */
+  protected Map<K, V> makeEitherMap() {
+    try {
+      return makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return makeEmptyMap();
+    }
+  }
+
+  protected final boolean supportsValuesHashCode(Map<K, V> map) {
+    // get the first non-null value
+    Collection<V> values = map.values();
+    for (V value : values) {
+      if (value != null) {
+        try {
+          value.hashCode();
+        } catch (Exception e) {
+          return false;
+        }
+        return true;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Checks all the properties that should always hold of a map. Also calls
+   * {@link #assertMoreInvariants} to check invariants that are peculiar to
+   * specific implementations.
+   * 
+   * @see #assertMoreInvariants
+   * @param map the map to check.
+   */
+  protected final void assertInvariants(Map<K, V> map) {
+    Set<K> keySet = map.keySet();
+    Collection<V> valueCollection = map.values();
+    Set<Entry<K, V>> entrySet = map.entrySet();
+
+    assertEquals(map.size() == 0, map.isEmpty());
+    assertEquals(map.size(), keySet.size());
+    assertEquals(keySet.size() == 0, keySet.isEmpty());
+    assertEquals(!keySet.isEmpty(), keySet.iterator().hasNext());
+
+    int expectedKeySetHash = 0;
+    for (K key : keySet) {
+      V value = map.get(key);
+      expectedKeySetHash += key != null ? key.hashCode() : 0;
+      assertTrue(map.containsKey(key));
+      assertTrue(map.containsValue(value));
+      assertTrue(valueCollection.contains(value));
+      assertTrue(valueCollection.containsAll(Collections.singleton(value)));
+      assertTrue(entrySet.contains(mapEntry(key, value)));
+      assertTrue(allowsNullKeys || (key != null));
+    }
+    assertEquals(expectedKeySetHash, keySet.hashCode());
+
+    assertEquals(map.size(), valueCollection.size());
+    assertEquals(valueCollection.size() == 0, valueCollection.isEmpty());
+    assertEquals(!valueCollection.isEmpty(),
+        valueCollection.iterator().hasNext());
+    for (V value : valueCollection) {
+      assertTrue(map.containsValue(value));
+      assertTrue(allowsNullValues || (value != null));
+    }
+
+    assertEquals(map.size(), entrySet.size());
+    assertEquals(entrySet.size() == 0, entrySet.isEmpty());
+    assertEquals(!entrySet.isEmpty(), entrySet.iterator().hasNext());
+    assertFalse(entrySet.contains("foo"));
+
+    boolean supportsValuesHashCode = supportsValuesHashCode(map);
+    if (supportsValuesHashCode) {
+      int expectedEntrySetHash = 0;
+      for (Entry<K, V> entry : entrySet) {
+        assertTrue(map.containsKey(entry.getKey()));
+        assertTrue(map.containsValue(entry.getValue()));
+        int expectedHash = (entry.getKey() == null ? 0
+            : entry.getKey().hashCode())
+            ^ (entry.getValue() == null ? 0 : entry.getValue().hashCode());
+        assertEquals(expectedHash, entry.hashCode());
+        expectedEntrySetHash += expectedHash;
+      }
+      assertEquals(expectedEntrySetHash, entrySet.hashCode());
+      assertTrue(entrySet.containsAll(new HashSet<Entry<K, V>>(entrySet)));
+      assertTrue(entrySet.equals(new HashSet<Entry<K, V>>(entrySet)));
+    }
+
+    Object[] entrySetToArray1 = entrySet.toArray();
+    assertEquals(map.size(), entrySetToArray1.length);
+    assertTrue(Arrays.asList(entrySetToArray1).containsAll(entrySet));
+
+    Entry<?, ?>[] entrySetToArray2 = new Entry<?, ?>[map.size() + 2];
+    entrySetToArray2[map.size()] = mapEntry("foo", 1);
+    assertSame(entrySetToArray2, entrySet.toArray(entrySetToArray2));
+    assertNull(entrySetToArray2[map.size()]);
+    assertTrue(Arrays.asList(entrySetToArray2).containsAll(entrySet));
+
+    Object[] valuesToArray1 = valueCollection.toArray();
+    assertEquals(map.size(), valuesToArray1.length);
+    assertTrue(Arrays.asList(valuesToArray1).containsAll(valueCollection));
+
+    Object[] valuesToArray2 = new Object[map.size() + 2];
+    valuesToArray2[map.size()] = "foo";
+    assertSame(valuesToArray2, valueCollection.toArray(valuesToArray2));
+    assertNull(valuesToArray2[map.size()]);
+    assertTrue(Arrays.asList(valuesToArray2).containsAll(valueCollection));
+
+    if (supportsValuesHashCode) {
+      int expectedHash = 0;
+      for (Entry<K, V> entry : entrySet) {
+        expectedHash += entry.hashCode();
+      }
+      assertEquals(expectedHash, map.hashCode());
+    }
+
+    assertMoreInvariants(map);
+  }
+
+  /**
+   * Override this to check invariants which should hold true for a particular
+   * implementation, but which are not generally applicable to every instance of
+   * Map.
+   * 
+   * @param map the map whose additional invariants to check.
+   */
+  protected void assertMoreInvariants(Map<K, V> map) {
+  }
+
+  public void testClear() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    if (supportsClear) {
+      map.clear();
+      assertTrue(map.isEmpty());
+    } else {
+      try {
+        map.clear();
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testContainsKey() {
+    final Map<K, V> map;
+    final K unmappedKey;
+    try {
+      map = makePopulatedMap();
+      unmappedKey = getKeyNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertFalse(map.containsKey(unmappedKey));
+    assertTrue(map.containsKey(map.keySet().iterator().next()));
+    if (allowsNullKeys) {
+      map.containsKey(null);
+    } else {
+      try {
+        map.containsKey(null);
+      } catch (JavaScriptException optional) {
+      } catch (NullPointerException e) {
+        // Expected in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testContainsValue() {
+    final Map<K, V> map;
+    final V unmappedValue;
+    try {
+      map = makePopulatedMap();
+      unmappedValue = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertFalse(map.containsValue(unmappedValue));
+    assertTrue(map.containsValue(map.values().iterator().next()));
+    if (allowsNullValues) {
+      map.containsValue(null);
+    } else {
+      try {
+        map.containsKey(null);
+      } catch (JavaScriptException optional) {
+      } catch (NullPointerException e) {
+        // Expected in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySet() {
+    final Map<K, V> map;
+    final Set<Entry<K, V>> entrySet;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertInvariants(map);
+
+    entrySet = map.entrySet();
+    final K unmappedKey;
+    final V unmappedValue;
+    try {
+      unmappedKey = getKeyNotInPopulatedMap();
+      unmappedValue = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    for (Entry<K, V> entry : entrySet) {
+      assertFalse(unmappedKey.equals(entry.getKey()));
+      assertFalse(unmappedValue.equals(entry.getValue()));
+    }
+  }
+
+  public void testEntrySetForEmptyMap() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetContainsEntryNullKeyPresent() {
+    if (!allowsNullKeys || !supportsPut) {
+      return;
+    }
+    final Map<K, V> map;
+    final Set<Entry<K, V>> entrySet;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertInvariants(map);
+
+    entrySet = map.entrySet();
+    final V unmappedValue;
+    try {
+      unmappedValue = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    map.put(null, unmappedValue);
+    Entry<K, V> entry = mapEntry(null, unmappedValue);
+    assertTrue(entrySet.contains(entry));
+    assertFalse(entrySet.contains(mapEntry(null, null)));
+  }
+
+  public void testEntrySetContainsEntryNullKeyMissing() {
+    final Map<K, V> map;
+    final Set<Entry<K, V>> entrySet;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertInvariants(map);
+
+    entrySet = map.entrySet();
+    final V unmappedValue;
+    try {
+      unmappedValue = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    Entry<K, V> entry = mapEntry(null, unmappedValue);
+    assertFalse(entrySet.contains(entry));
+    assertFalse(entrySet.contains(mapEntry(null, null)));
+  }
+
+  public void testEntrySetIteratorRemove() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    Iterator<Entry<K, V>> iterator = entrySet.iterator();
+    if (supportsIteratorRemove) {
+      int initialSize = map.size();
+      Entry<K, V> entry = iterator.next();
+      iterator.remove();
+      assertEquals(initialSize - 1, map.size());
+      assertFalse(entrySet.contains(entry));
+      assertInvariants(map);
+      try {
+        iterator.remove();
+        fail("Expected IllegalStateException.");
+      } catch (IllegalStateException e) {
+        // Expected.
+      }
+    } else {
+      try {
+        iterator.next();
+        iterator.remove();
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetRemove() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    if (supportsRemove) {
+      int initialSize = map.size();
+      boolean didRemove = entrySet.remove(entrySet.iterator().next());
+      assertTrue(didRemove);
+      assertEquals(initialSize - 1, map.size());
+    } else {
+      try {
+        entrySet.remove(entrySet.iterator().next());
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetRemoveMissingKey() {
+    final Map<K, V> map;
+    final K key;
+    try {
+      map = makeEitherMap();
+      key = getKeyNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    Entry<K, V> entry = mapEntry(key, getValueNotInPopulatedMap());
+    int initialSize = map.size();
+    if (supportsRemove) {
+      boolean didRemove = entrySet.remove(entry);
+      assertFalse(didRemove);
+    } else {
+      try {
+        boolean didRemove = entrySet.remove(entry);
+        assertFalse(didRemove);
+      } catch (UnsupportedOperationException optional) {
+      }
+    }
+    assertEquals(initialSize, map.size());
+    assertFalse(map.containsKey(key));
+    assertInvariants(map);
+  }
+
+  public void testEntrySetRemoveDifferentValue() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    K key = map.keySet().iterator().next();
+    Entry<K, V> entry = mapEntry(key, getValueNotInPopulatedMap());
+    int initialSize = map.size();
+    if (supportsRemove) {
+      boolean didRemove = entrySet.remove(entry);
+      assertFalse(didRemove);
+    } else {
+      try {
+        boolean didRemove = entrySet.remove(entry);
+        assertFalse(didRemove);
+      } catch (UnsupportedOperationException optional) {
+      }
+    }
+    assertEquals(initialSize, map.size());
+    assertTrue(map.containsKey(key));
+    assertInvariants(map);
+  }
+
+  public void testEntrySetRemoveNullKeyPresent() {
+    if (!allowsNullKeys || !supportsPut || !supportsRemove) {
+      return;
+    }
+    final Map<K, V> map;
+    final Set<Entry<K, V>> entrySet;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertInvariants(map);
+
+    entrySet = map.entrySet();
+    final V unmappedValue;
+    try {
+      unmappedValue = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    map.put(null, unmappedValue);
+    assertEquals(unmappedValue, map.get(null));
+    assertTrue(map.containsKey(null));
+    Entry<K, V> entry = mapEntry(null, unmappedValue);
+    assertTrue(entrySet.remove(entry));
+    assertNull(map.get(null));
+    assertFalse(map.containsKey(null));
+  }
+
+  public void testEntrySetRemoveNullKeyMissing() {
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    Entry<K, V> entry = mapEntry(null, getValueNotInPopulatedMap());
+    int initialSize = map.size();
+    if (supportsRemove) {
+      boolean didRemove = entrySet.remove(entry);
+      assertFalse(didRemove);
+    } else {
+      try {
+        boolean didRemove = entrySet.remove(entry);
+        assertFalse(didRemove);
+      } catch (UnsupportedOperationException optional) {
+      }
+    }
+    assertEquals(initialSize, map.size());
+    assertInvariants(map);
+  }
+
+  public void testEntrySetRemoveAll() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    Set<Entry<K, V>> entriesToRemove = singleton(entrySet.iterator().next());
+    if (supportsRemove) {
+      int initialSize = map.size();
+      boolean didRemove = entrySet.removeAll(entriesToRemove);
+      assertTrue(didRemove);
+      assertEquals(initialSize - entriesToRemove.size(), map.size());
+      for (Entry<K, V> entry : entriesToRemove) {
+        assertFalse(entrySet.contains(entry));
+      }
+    } else {
+      try {
+        entrySet.removeAll(entriesToRemove);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetRemoveAllNullFromEmpty() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    if (supportsRemove) {
+      try {
+        entrySet.removeAll(null);
+        fail("Expected JavaScriptException.");
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      } catch (NullPointerException e) {
+        // Expected in GWT client.
+      }
+    } else {
+      try {
+        entrySet.removeAll(null);
+        fail("Expected UnsupportedOperationException or JavaScriptException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      } catch (NullPointerException e) {
+        // Expected in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetRetainAll() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    Set<Entry<K, V>> entriesToRetain = singleton(entrySet.iterator().next());
+    if (supportsRemove) {
+      boolean shouldRemove = (entrySet.size() > entriesToRetain.size());
+      boolean didRemove = entrySet.retainAll(entriesToRetain);
+      assertEquals(shouldRemove, didRemove);
+      assertEquals(entriesToRetain.size(), map.size());
+      for (Entry<K, V> entry : entriesToRetain) {
+        assertTrue(entrySet.contains(entry));
+      }
+    } else {
+      try {
+        entrySet.retainAll(entriesToRetain);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetRetainAllNullFromEmpty() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    if (supportsRemove) {
+      try {
+        entrySet.retainAll(null);
+        // Returning successfully is not ideal, but tolerated.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      }
+    } else {
+      try {
+        entrySet.retainAll(null);
+        // We have to tolerate a successful return (Sun bug 4802647)
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetClear() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    if (supportsClear) {
+      entrySet.clear();
+      assertTrue(entrySet.isEmpty());
+    } else {
+      try {
+        entrySet.clear();
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetAddAndAddAll() {
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return; 
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    final Entry<K, V> entryToAdd = mapEntry(null, null);
+    try {
+      entrySet.add(entryToAdd);
+      fail("Expected UnsupportedOperationException or JavaScriptException.");
+    } catch (UnsupportedOperationException e) {
+      // Expected.
+    } catch (JavaScriptException e) {
+      // Expected in GWT client.
+    }
+    assertInvariants(map);
+
+    try {
+      entrySet.addAll(singleton(entryToAdd));
+      fail("Expected UnsupportedOperationException or JavaScriptException.");
+    } catch (UnsupportedOperationException e) {
+      // Expected.
+    } catch (JavaScriptException e) {
+      // Expected in GWT client.
+    }
+    assertInvariants(map);
+  }
+
+  public void testEntrySetSetValue() {
+    // TODO: Investigate the extent to which, in practice, maps that support
+    // put() also support Entry.setValue().
+    if (!supportsPut) {
+      return;
+    }
+
+    final Map<K, V> map;
+    final V valueToSet;
+    try {
+      map = makePopulatedMap();
+      valueToSet = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    Entry<K, V> entry = entrySet.iterator().next();
+    final V oldValue = entry.getValue();
+    final V returnedValue = entry.setValue(valueToSet);
+    assertEquals(oldValue, returnedValue);
+    assertTrue(entrySet.contains(mapEntry(entry.getKey(), valueToSet)));
+    assertEquals(valueToSet, map.get(entry.getKey()));
+    assertInvariants(map);
+  }
+
+  public void testEntrySetSetValueSameValue() {
+    // TODO: Investigate the extent to which, in practice, maps that support
+    // put() also support Entry.setValue().
+    if (!supportsPut) {
+      return;
+    }
+
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<Entry<K, V>> entrySet = map.entrySet();
+    Entry<K, V> entry = entrySet.iterator().next();
+    final V oldValue = entry.getValue();
+    final V returnedValue = entry.setValue(oldValue);
+    assertEquals(oldValue, returnedValue);
+    assertTrue(entrySet.contains(mapEntry(entry.getKey(), oldValue)));
+    assertEquals(oldValue, map.get(entry.getKey()));
+    assertInvariants(map);
+  }
+
+  public void testEqualsForEqualMap() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    assertEquals(map, map);
+    assertEquals(makePopulatedMap(), map);
+    assertFalse(map.equals(Collections.emptyMap()));
+    // no-inspection ObjectEqualsNull
+    assertFalse(map.equals(null));
+  }
+
+  /*
+   * equals does not apply to Storage because there's only one instance so two 
+   * maps will always be equal.
+   */
+  public void disabled_testEqualsForLargerMap() {
+    if (!supportsPut) {
+      return;
+    }
+
+    final Map<K, V> map;
+    final Map<K, V> largerMap;
+    try {
+      map = makePopulatedMap();
+      largerMap = makePopulatedMap();
+      largerMap.put(getKeyNotInPopulatedMap(), getValueNotInPopulatedMap());
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    assertFalse(map.equals(largerMap));
+  }
+
+  /*
+   * equals does not apply to Storage because there's only one instance so two 
+   * maps will always be equal.
+   */
+  public void disabled_testEqualsForSmallerMap() {
+    if (!supportsRemove) {
+      return;
+    }
+
+    final Map<K, V> map;
+    final Map<K, V> smallerMap;
+    try {
+      map = makePopulatedMap();
+      smallerMap = makePopulatedMap();
+      smallerMap.remove(smallerMap.keySet().iterator().next());
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    
+    assertFalse(map.equals(smallerMap));
+  }
+
+  public void testEqualsForEmptyMap() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    assertEquals(map, map);
+    assertEquals(makeEmptyMap(), map);
+    assertEquals(Collections.emptyMap(), map);
+    assertFalse(map.equals(Collections.emptySet()));
+    // noinspection ObjectEqualsNull
+    assertFalse(map.equals(null));
+  }
+
+  public void testGet() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    for (Entry<K, V> entry : map.entrySet()) {
+      assertEquals(entry.getValue(), map.get(entry.getKey()));
+    }
+
+    K unmappedKey = null;
+    try {
+      unmappedKey = getKeyNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertNull(map.get(unmappedKey));
+  }
+
+  public void testGetForEmptyMap() {
+    final Map<K, V> map;
+    K unmappedKey = null;
+    try {
+      map = makeEmptyMap();
+      unmappedKey = getKeyNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertNull(map.get(unmappedKey));
+  }
+
+  public void testGetNull() {
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return; 
+    }
+    
+    if (allowsNullKeys) {
+      if (allowsNullValues) {
+        // TODO: decide what to test here.
+      } else {
+        assertEquals(map.containsKey(null), map.get(null) != null);
+      }
+    } else {
+      try {
+        map.get(null);
+      } catch (JavaScriptException optional) {
+        // in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testHashCode() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertInvariants(map);
+  }
+
+  public void testHashCodeForEmptyMap() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertInvariants(map);
+  }
+
+  public void testPutNewKey() {
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return; 
+    }
+    
+    final K keyToPut;
+    final V valueToPut;
+    try {
+      keyToPut = getKeyNotInPopulatedMap();
+      valueToPut = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    if (supportsPut) {
+      int initialSize = map.size();
+      V oldValue = map.put(keyToPut, valueToPut);
+      assertEquals(valueToPut, map.get(keyToPut));
+      assertTrue(map.containsKey(keyToPut));
+      assertTrue(map.containsValue(valueToPut));
+      assertEquals(initialSize + 1, map.size());
+      assertNull(oldValue);
+    } else {
+      try {
+        map.put(keyToPut, valueToPut);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testPutExistingKey() {
+    final Map<K, V> map;
+    final K keyToPut;
+    final V valueToPut;
+    try {
+      map = makePopulatedMap();
+      valueToPut = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    keyToPut = map.keySet().iterator().next();
+    if (supportsPut) {
+      int initialSize = map.size();
+      map.put(keyToPut, valueToPut);
+      assertEquals(valueToPut, map.get(keyToPut));
+      assertTrue(map.containsKey(keyToPut));
+      assertTrue(map.containsValue(valueToPut));
+      assertEquals(initialSize, map.size());
+    } else {
+      try {
+        map.put(keyToPut, valueToPut);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testPutNullKey() {
+    if (!supportsPut) {
+      return;
+    }
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return; 
+    }
+    final V valueToPut;
+    try {
+      valueToPut = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    if (allowsNullKeys) {
+      final V oldValue = map.get(null);
+      final V returnedValue = map.put(null, valueToPut);
+      assertEquals(oldValue, returnedValue);
+      assertEquals(valueToPut, map.get(null));
+      assertTrue(map.containsKey(null));
+      assertTrue(map.containsValue(valueToPut));
+    } else {
+      try {
+        map.put(null, valueToPut);
+        fail("Expected RuntimeException");
+      } catch (RuntimeException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testPutNullValue() {
+    if (!supportsPut) {
+      return;
+    }
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return; 
+    }
+    final K keyToPut;
+    try {
+      keyToPut = getKeyNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    if (allowsNullValues) {
+      int initialSize = map.size();
+      final V oldValue = map.get(keyToPut);
+      final V returnedValue = map.put(keyToPut, null);
+      assertEquals(oldValue, returnedValue);
+      assertNull(map.get(keyToPut));
+      assertTrue(map.containsKey(keyToPut));
+      assertTrue(map.containsValue(null));
+      assertEquals(initialSize + 1, map.size());
+    } else {
+      try {
+        map.put(keyToPut, null);
+        fail("Expected RuntimeException");
+      } catch (RuntimeException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testPutNullValueForExistingKey() {
+    if (!supportsPut) {
+      return;
+    }
+    final Map<K, V> map;
+    final K keyToPut;
+    try {
+      map = makePopulatedMap();
+      keyToPut = map.keySet().iterator().next();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    if (allowsNullValues) {
+      int initialSize = map.size();
+      final V oldValue = map.get(keyToPut);
+      final V returnedValue = map.put(keyToPut, null);
+      assertEquals(oldValue, returnedValue);
+      assertNull(map.get(keyToPut));
+      assertTrue(map.containsKey(keyToPut));
+      assertTrue(map.containsValue(null));
+      assertEquals(initialSize, map.size());
+    } else {
+      try {
+        map.put(keyToPut, null);
+        fail("Expected RuntimeException");
+      } catch (RuntimeException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testPutAllNewKey() {
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return; 
+    }
+    final K keyToPut;
+    final V valueToPut;
+    try {
+      keyToPut = getKeyNotInPopulatedMap();
+      valueToPut = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    final Map<K, V> mapToPut = Collections.singletonMap(keyToPut, valueToPut);
+    if (supportsPut) {
+      int initialSize = map.size();
+      map.putAll(mapToPut);
+      assertEquals(valueToPut, map.get(keyToPut));
+      assertTrue(map.containsKey(keyToPut));
+      assertTrue(map.containsValue(valueToPut));
+      assertEquals(initialSize + 1, map.size());
+    } else {
+      try {
+        map.putAll(mapToPut);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testPutAllExistingKey() {
+    final Map<K, V> map;
+    final K keyToPut;
+    final V valueToPut;
+    try {
+      map = makePopulatedMap();
+      valueToPut = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    keyToPut = map.keySet().iterator().next();
+    final Map<K, V> mapToPut = Collections.singletonMap(keyToPut, valueToPut);
+    int initialSize = map.size();
+    if (supportsPut) {
+      map.putAll(mapToPut);
+      assertEquals(valueToPut, map.get(keyToPut));
+      assertTrue(map.containsKey(keyToPut));
+      assertTrue(map.containsValue(valueToPut));
+    } else {
+      try {
+        map.putAll(mapToPut);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertEquals(initialSize, map.size());
+    assertInvariants(map);
+  }
+
+  public void testRemove() {
+    final Map<K, V> map;
+    final K keyToRemove;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    keyToRemove = map.keySet().iterator().next();
+    if (supportsRemove) {
+      int initialSize = map.size();
+      V expectedValue = map.get(keyToRemove);
+      V oldValue = map.remove(keyToRemove);
+      assertEquals(expectedValue, oldValue);
+      assertFalse(map.containsKey(keyToRemove));
+      assertEquals(initialSize - 1, map.size());
+    } else {
+      try {
+        map.remove(keyToRemove);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testRemoveMissingKey() {
+    final Map<K, V> map;
+    final K keyToRemove;
+    try {
+      map = makePopulatedMap();
+      keyToRemove = getKeyNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    if (supportsRemove) {
+      int initialSize = map.size();
+      assertNull(map.remove(keyToRemove));
+      assertEquals(initialSize, map.size());
+    } else {
+      try {
+        map.remove(keyToRemove);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testSize() {
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return; 
+    }
+    assertInvariants(map);
+  }
+
+  public void testKeySetClear() {
+    final Map<K, V> map;
+    try {
+      map = makeEitherMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<K> keySet = map.keySet();
+    if (supportsClear) {
+      keySet.clear();
+      assertTrue(keySet.isEmpty());
+    } else {
+      try {
+        keySet.clear();
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testKeySetRemoveAllNullFromEmpty() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<K> keySet = map.keySet();
+    if (supportsRemove) {
+      try {
+        keySet.removeAll(null);
+        fail("Expected JavaScriptException.");
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      } catch (NullPointerException e) {
+        // Expected in GWT client.
+      }
+    } else {
+      try {
+        keySet.removeAll(null);
+        fail("Expected UnsupportedOperationException or JavaScriptException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      } catch (NullPointerException e) {
+        // Expected in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testKeySetRetainAllNullFromEmpty() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Set<K> keySet = map.keySet();
+    if (supportsRemove) {
+      try {
+        keySet.retainAll(null);
+        // Returning successfully is not ideal, but tolerated.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      }
+    } else {
+      try {
+        keySet.retainAll(null);
+        // We have to tolerate a successful return (Sun bug 4802647)
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testValues() {
+    final Map<K, V> map;
+    final Collection<V> valueCollection;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    assertInvariants(map);
+
+    valueCollection = map.values();
+    final V unmappedValue;
+    try {
+      unmappedValue = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+    for (V value : valueCollection) {
+      assertFalse(unmappedValue.equals(value));
+    }
+  }
+
+  public void testValuesIteratorRemove() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Collection<V> valueCollection = map.values();
+    Iterator<V> iterator = valueCollection.iterator();
+    if (supportsIteratorRemove) {
+      int initialSize = map.size();
+      iterator.next();
+      iterator.remove();
+      assertEquals(initialSize - 1, map.size());
+      // (We can't assert that the values collection no longer contains the
+      // removed value, because the underlying map can have multiple mappings
+      // to the same value.)
+      assertInvariants(map);
+      try {
+        iterator.remove();
+        fail("Expected IllegalStateException.");
+      } catch (IllegalStateException e) {
+        // Expected.
+      }
+    } else {
+      try {
+        iterator.next();
+        iterator.remove();
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testValuesRemove() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Collection<V> valueCollection = map.values();
+    if (supportsRemove) {
+      int initialSize = map.size();
+      valueCollection.remove(valueCollection.iterator().next());
+      assertEquals(initialSize - 1, map.size());
+      // (We can't assert that the values collection no longer contains the
+      // removed value, because the underlying map can have multiple mappings
+      // to the same value.)
+    } else {
+      try {
+        valueCollection.remove(valueCollection.iterator().next());
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testValuesRemoveMissing() {
+    final Map<K, V> map;
+    final V valueToRemove;
+    try {
+      map = makeEitherMap();
+      valueToRemove = getValueNotInPopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Collection<V> valueCollection = map.values();
+    int initialSize = map.size();
+    if (supportsRemove) {
+      assertFalse(valueCollection.remove(valueToRemove));
+    } else {
+      try {
+        assertFalse(valueCollection.remove(valueToRemove));
+      } catch (UnsupportedOperationException e) {
+        // Tolerated.
+      }
+    }
+    assertEquals(initialSize, map.size());
+    assertInvariants(map);
+  }
+
+  public void testValuesRemoveAll() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Collection<V> valueCollection = map.values();
+    Set<V> valuesToRemove = singleton(valueCollection.iterator().next());
+    if (supportsRemove) {
+      valueCollection.removeAll(valuesToRemove);
+      for (V value : valuesToRemove) {
+        assertFalse(valueCollection.contains(value));
+      }
+      for (V value : valueCollection) {
+        assertFalse(valuesToRemove.contains(value));
+      }
+    } else {
+      try {
+        valueCollection.removeAll(valuesToRemove);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testValuesRemoveAllNullFromEmpty() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Collection<V> values = map.values();
+    if (supportsRemove) {
+      try {
+        values.removeAll(null);
+        // Returning successfully is not ideal, but tolerated.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      }
+    } else {
+      try {
+        values.removeAll(null);
+        // We have to tolerate a successful return (Sun bug 4802647)
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testValuesRetainAll() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Collection<V> valueCollection = map.values();
+    Set<V> valuesToRetain = singleton(valueCollection.iterator().next());
+    if (supportsRemove) {
+      valueCollection.retainAll(valuesToRetain);
+      for (V value : valuesToRetain) {
+        assertTrue(valueCollection.contains(value));
+      }
+      for (V value : valueCollection) {
+        assertTrue(valuesToRetain.contains(value));
+      }
+    } else {
+      try {
+        valueCollection.retainAll(valuesToRetain);
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testValuesRetainAllNullFromEmpty() {
+    final Map<K, V> map;
+    try {
+      map = makeEmptyMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Collection<V> values = map.values();
+    if (supportsRemove) {
+      try {
+        values.retainAll(null);
+        // Returning successfully is not ideal, but tolerated.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      }
+    } else {
+      try {
+        values.retainAll(null);
+        // We have to tolerate a successful return (Sun bug 4802647)
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      } catch (JavaScriptException e) {
+        // Expected in GWT client.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  public void testValuesClear() {
+    final Map<K, V> map;
+    try {
+      map = makePopulatedMap();
+    } catch (UnsupportedOperationException e) {
+      return;
+    }
+
+    Collection<V> valueCollection = map.values();
+    if (supportsClear) {
+      valueCollection.clear();
+      assertTrue(valueCollection.isEmpty());
+    } else {
+      try {
+        valueCollection.clear();
+        fail("Expected UnsupportedOperationException.");
+      } catch (UnsupportedOperationException e) {
+        // Expected.
+      }
+    }
+    assertInvariants(map);
+  }
+
+  private static <K, V> Entry<K, V> mapEntry(K key, V value) {
+    return Collections.singletonMap(key, value).entrySet().iterator().next();
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/storage/client/SessionStorageMapTest.java b/user/test/com/google/gwt/storage/client/SessionStorageMapTest.java
new file mode 100644
index 0000000..8a3fbb9
--- /dev/null
+++ b/user/test/com/google/gwt/storage/client/SessionStorageMapTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+/**
+ * Tests Session {@link StorageMap}.
+ * 
+ * Because HtmlUnit does not support Storage, you will need to run these tests
+ * manually by adding this line to your VM args: -Dgwt.args="-runStyle Manual:1"
+ * If you are using Eclipse and GPE, go to "run configurations" or
+ * "debug configurations", select the test you would like to run, and put this
+ * line in the VM args under the arguments tab: -Dgwt.args="-runStyle Manual:1"
+ */
+public class SessionStorageMapTest extends StorageMapTest {
+  @Override
+  Storage getStorage() {
+    return Storage.getLocalStorageIfSupported();
+  }
+}
diff --git a/user/test/com/google/gwt/storage/client/SessionStorageTest.java b/user/test/com/google/gwt/storage/client/SessionStorageTest.java
new file mode 100644
index 0000000..1ecb3e2
--- /dev/null
+++ b/user/test/com/google/gwt/storage/client/SessionStorageTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests Session {@link Storage}.
+ * 
+ * Because HtmlUnit does not support Storage, you will need to run these tests
+ * manually by adding this line to your VM args: -Dgwt.args="-runStyle Manual:1"
+ * If you are using Eclipse and GPE, go to "run configurations" or
+ * "debug configurations", select the test you would like to run, and put this
+ * line in the VM args under the arguments tab: -Dgwt.args="-runStyle Manual:1"
+ */
+@DoNotRunWith(Platform.HtmlUnitUnknown)
+public class SessionStorageTest extends StorageTest {
+  @Override
+  Storage getStorage() {
+    return Storage.getSessionStorageIfSupported();
+  }
+}
diff --git a/user/test/com/google/gwt/storage/client/StorageMapTest.java b/user/test/com/google/gwt/storage/client/StorageMapTest.java
new file mode 100644
index 0000000..a61c4c4
--- /dev/null
+++ b/user/test/com/google/gwt/storage/client/StorageMapTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+import java.util.Map;
+
+/**
+ * Tests {@link StorageMap}
+ * 
+ * Because HtmlUnit does not support Storage, you will need to run these tests
+ * manually by adding this line to your VM args: -Dgwt.args="-runStyle Manual:1"
+ * If you are using Eclipse and GPE, go to "run configurations" or
+ * "debug configurations", select the test you would like to run, and put this
+ * line in the VM args under the arguments tab: -Dgwt.args="-runStyle Manual:1"
+ */
+@DoNotRunWith(Platform.HtmlUnitUnknown)
+public abstract class StorageMapTest extends MapInterfaceTest<String, String> {
+  protected Storage storage;
+
+  public StorageMapTest() {
+    super(false, false, true, true, true);
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.storage.Storage";
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    storage = getStorage();
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    // setup for tests by emptying storage
+    storage.clear();
+  }
+
+  /**
+   * Returns a {@link Storage} object.
+   * 
+   * Override to return either a LocalStorage or a SessionStorage
+   * 
+   * @return a {@link Storage} object
+   */
+  abstract Storage getStorage();
+
+  @Override
+  protected String getKeyNotInPopulatedMap()
+      throws UnsupportedOperationException {
+    return "nonExistingKey";
+  }
+
+  @Override
+  protected String getValueNotInPopulatedMap()
+      throws UnsupportedOperationException {
+    return "nonExistingValue";
+  }
+
+  @Override
+  protected Map<String, String> makeEmptyMap()
+      throws UnsupportedOperationException {
+    if (storage == null) {
+      throw new UnsupportedOperationException("StorageMap not supported because Storage is not supported.");
+    }
+
+    storage.clear();
+    
+    return new StorageMap(storage);
+  }
+
+  @Override
+  protected Map<String, String> makePopulatedMap()
+      throws UnsupportedOperationException {
+    if (storage == null) {
+      throw new UnsupportedOperationException("StorageMap not supported because Storage is not supported.");
+    }
+
+    storage.clear();
+    
+    storage.setItem("one", "January");
+    storage.setItem("two", "February");
+    storage.setItem("three", "March");
+    storage.setItem("four", "April");
+    storage.setItem("five", "May");
+    
+    return new StorageMap(storage);
+  }
+}
diff --git a/user/test/com/google/gwt/storage/client/StorageTest.java b/user/test/com/google/gwt/storage/client/StorageTest.java
new file mode 100644
index 0000000..e98454a
--- /dev/null
+++ b/user/test/com/google/gwt/storage/client/StorageTest.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2011 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.storage.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.Timer;
+
+/**
+ * Tests {@link Storage}
+ * 
+ * Because HtmlUnit does not support Storage, you will need to run these tests
+ * manually by adding this line to your VM args: -Dgwt.args="-runStyle Manual:1"
+ * If you are using Eclipse and GPE, go to "run configurations" or
+ * "debug configurations", select the test you would like to run, and put this
+ * line in the VM args under the arguments tab: -Dgwt.args="-runStyle Manual:1"
+ */
+@DoNotRunWith(Platform.HtmlUnitUnknown)
+public abstract class StorageTest extends GWTTestCase {
+  protected Storage storage;
+  protected StorageEvent.Handler handler;
+  protected StorageEvent.Handler handler2;
+
+  private native boolean isFirefox35OrLater() /*-{
+    var geckoVersion = @com.google.gwt.dom.client.DOMImplMozilla::getGeckoVersion()();
+    return (geckoVersion != -1) && (geckoVersion >= 1009001);
+  }-*/;
+
+  private native boolean isSafari3OrBefore() /*-{
+    return @com.google.gwt.dom.client.DOMImplSafari::isWebkit525OrBefore()();
+  }-*/;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.storage.Storage";
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    storage = getStorage();
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    // setup for tests by removing event handler
+    if (handler != null) {
+      storage.removeStorageEventHandler(handler);
+      handler = null;
+    }
+    if (handler2 != null) {
+      storage.removeStorageEventHandler(handler2);
+      handler2 = null;
+    }
+
+    // setup for tests by emptying storage
+    storage.clear();
+  }
+
+  @Override
+  protected void gwtTearDown() throws Exception {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    // clean up by removing event handler
+    if (handler != null) {
+      storage.removeStorageEventHandler(handler);
+      handler = null;
+    }
+    if (handler2 != null) {
+      storage.removeStorageEventHandler(handler2);
+      handler2 = null;
+    }
+
+    // clean up by emptying storage
+    storage.clear();
+  }
+
+  /**
+   * Returns a {@link Storage} object.
+   * 
+   * Override to return either a LocalStorage or a SessionStorage
+   * 
+   * @return a {@link Storage} object
+   */
+  abstract Storage getStorage();
+
+  public void testClear() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    storage.setItem("foo", "bar");
+    assertEquals(1, storage.getLength());
+    storage.clear();
+    assertEquals(0, storage.getLength());
+  }
+
+  public void testGet() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    storage.setItem("foo1", "bar1");
+    storage.setItem("foo2", "bar2");
+    assertEquals("bar1", storage.getItem("foo1"));
+    assertEquals("bar2", storage.getItem("foo2"));
+
+    // getting a value of a key that hasn't been set should return null
+    assertNull(
+        "Getting a value of a key that hasn't been set should return null",
+        storage.getItem("notset"));
+  }
+
+  public void testLength() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    storage.clear();
+    assertEquals(0, storage.getLength());
+    storage.setItem("abc", "def");
+    assertEquals(1, storage.getLength());
+    storage.setItem("ghi", "jkl");
+    assertEquals(2, storage.getLength());
+    storage.clear();
+    assertEquals(0, storage.getLength());
+  }
+
+  public void testSet() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    assertEquals(null, storage.getItem("foo"));
+    assertEquals(0, storage.getLength());
+    storage.setItem("foo", "bar1");
+    assertEquals("bar1", storage.getItem("foo"));
+    assertEquals(1, storage.getLength());
+    storage.setItem("foo", "bar2");
+    assertEquals("Should be able to overwrite an existing value", "bar2",
+        storage.getItem("foo"));
+    assertEquals(1, storage.getLength());
+
+    // test that using the empty string as a key throws an exception in devmode
+    if (!GWT.isScript()) {
+      try {
+        storage.setItem("", "baz");
+        fail("Empty string should be disallowed as a key.");
+      } catch (AssertionError e) {
+        // expected
+      }
+    }
+  }
+
+  public void testKey() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    // key(n) where n >= storage.length() should return null
+    assertEquals(null, storage.key(0));
+    storage.setItem("a", "b");
+    assertEquals(null, storage.key(1));
+    storage.clear();
+
+    storage.setItem("foo1", "bar");
+    assertEquals("foo1", storage.key(0));
+    storage.setItem("foo2", "bar");
+    // key(0) should be either foo1 or foo2
+    assertTrue(storage.key(0).equals("foo1") || storage.key(0).equals("foo2"));
+    // foo1 should be either key(0) or key(1)
+    assertTrue(storage.key(0).equals("foo1") || storage.key(1).equals("foo1"));
+    // foo2 should be either key(0) or key(1)
+    assertTrue(storage.key(0).equals("foo2") || storage.key(1).equals("foo2"));
+  }
+
+  public void testRemoveItem() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    storage.setItem("foo1", "bar1");
+    storage.setItem("foo2", "bar2");
+    assertEquals("bar1", storage.getItem("foo1"));
+    assertEquals("bar2", storage.getItem("foo2"));
+
+    // removing a non-existent key should have no effect
+    storage.removeItem("abc");
+    assertEquals("bar1", storage.getItem("foo1"));
+    assertEquals("bar2", storage.getItem("foo2"));
+
+    // removing a key should remove that key and value
+    storage.removeItem("foo1");
+    assertEquals(null, storage.getItem("foo1"));
+    assertEquals("bar2", storage.getItem("foo2"));
+    storage.removeItem("foo2");
+    assertEquals(null, storage.getItem("foo2"));
+  }
+
+  public void testClearStorageEvent() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    delayTestFinish(2000);
+    storage.setItem("tcseFoo", "tcseBar");
+    handler = new StorageEvent.Handler() {
+      public void onStorageChange(StorageEvent event) {
+        assertNull(event.getKey());
+        assertNull(event.getOldValue());
+        assertNull(event.getNewValue());
+        assertEquals(storage, event.getStorageArea());
+        assertNotNull(event.getUrl());
+
+        finishTest();
+      }
+    };
+    storage.addStorageEventHandler(handler);
+    storage.clear();
+  }
+
+  public void testSetItemStorageEvent() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    delayTestFinish(2000);
+    storage.setItem("tsiseFoo", "tsiseBarOld");
+
+    handler = new StorageEvent.Handler() {
+      public void onStorageChange(StorageEvent event) {
+        assertEquals("tsiseFoo", event.getKey());
+        assertEquals("tsiseBarNew", event.getNewValue());
+        assertEquals("tsiseBarOld", event.getOldValue());
+        assertEquals(storage, event.getStorageArea());
+        assertNotNull(event.getUrl());
+
+        finishTest();
+      }
+    };
+    storage.addStorageEventHandler(handler);
+    storage.setItem("tsiseFoo", "tsiseBarNew");
+  }
+
+  public void testRemoveItemStorageEvent() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    delayTestFinish(2000);
+    storage.setItem("triseFoo", "triseBarOld");
+
+    handler = new StorageEvent.Handler() {
+      public void onStorageChange(StorageEvent event) {
+        assertEquals("triseFoo", event.getKey());
+        finishTest();
+      }
+    };
+    storage.addStorageEventHandler(handler);
+    storage.removeItem("triseFoo");
+  }
+
+  public void testHandlerRegistration() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    final boolean[] eventFired = new boolean[1];
+    eventFired[0] = false;
+
+    delayTestFinish(3000);
+
+    handler = new StorageEvent.Handler() {
+      public void onStorageChange(StorageEvent event) {
+        fail("Storage change should not have fired.");
+        eventFired[0] = true;
+        finishTest();
+      }
+    };
+    HandlerRegistration registration = storage.addStorageEventHandler(handler);
+    registration.removeHandler();
+
+    // these should fire events, but they should not be caught by handler
+    storage.setItem("thrFoo", "thrBar");
+    storage.clear();
+
+    // schedule timer to make sure event didn't fire async
+    new Timer() {
+      @Override
+      public void run() {
+        if (!eventFired[0]) {
+          finishTest();
+        }
+      }
+    }.schedule(1000);
+  }
+
+  public void testEventInEvent() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    delayTestFinish(3000);
+    storage.setItem("teieFoo", "teieBar");
+
+    handler = new StorageEvent.Handler() {
+      public void onStorageChange(StorageEvent event) {
+        if ("teieFoo".equals(event.getKey())) {
+          storage.clear();
+          storage.setItem("teieFoo2", "teieBar2");
+          // firing events from within a handler should not corrupt the values.
+          assertEquals("teieFoo", event.getKey());
+          storage.setItem("teieFooEndTest", "thanks");
+        }
+        if ("teieFooEndTest".equals(event.getKey())) {
+          finishTest();
+        }
+      }
+    };
+    storage.addStorageEventHandler(handler);
+    storage.removeItem("teieFoo");
+  }
+
+  public void testMultipleEventHandlers() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    delayTestFinish(3000);
+
+    final int[] eventHandledCount = new int[]{0};
+
+    storage.setItem("tmehFoo", "tmehBar");
+
+    handler = new StorageEvent.Handler() {
+      public void onStorageChange(StorageEvent event) {
+        if ("tmehFoo".equals(event.getKey())) {
+          eventHandledCount[0]++;
+          if (eventHandledCount[0] == 2) {
+            finishTest();
+          }
+        }
+      }
+    };
+    storage.addStorageEventHandler(handler);
+
+    handler2 = new StorageEvent.Handler() {
+      public void onStorageChange(StorageEvent event) {
+        if ("tmehFoo".equals(event.getKey())) {
+          eventHandledCount[0]++;
+          if (eventHandledCount[0] == 2) {
+            finishTest();
+          }
+        }
+      }
+    };
+    storage.addStorageEventHandler(handler2);
+    storage.removeItem("tmehFoo");
+  }
+
+  public void testEventStorageArea() {
+    if (storage == null) {
+      return; // do not run if not supported
+    }
+
+    delayTestFinish(2000);
+    storage.setItem("tesaFoo", "tesaBar");
+    handler = new StorageEvent.Handler() {
+      public void onStorageChange(StorageEvent event) {
+        Storage eventStorage = event.getStorageArea();
+        assertEquals(storage, eventStorage);
+        boolean equalsLocal = Storage.getLocalStorageIfSupported().equals(
+            eventStorage);
+        boolean equalsSession = Storage.getSessionStorageIfSupported().equals(
+            eventStorage);
+        // assert that storage is either local or session, but not both.
+        assertFalse(equalsLocal == equalsSession);
+
+        finishTest();
+      }
+    };
+    storage.addStorageEventHandler(handler);
+    storage.clear();
+  }
+
+  public void testSupported() {
+    // test the isxxxSupported() call
+    if (isFirefox35OrLater()) {
+      assertNotNull(storage);
+      assertTrue(Storage.isLocalStorageSupported());
+      assertTrue(Storage.isSessionStorageSupported());
+      assertTrue(Storage.isSupported());
+    }
+    if (isSafari3OrBefore()) {
+      assertNull(storage);
+      assertFalse(Storage.isLocalStorageSupported());
+      assertFalse(Storage.isSessionStorageSupported());
+      assertFalse(Storage.isSupported());
+    }
+  }
+}