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<String, String>}.
+ *
+ * <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());
+ }
+ }
+}