Port of CacheDataTest.java plus bugfixes
diff --git a/src/com/android/i18n/addressinput/CacheData.java b/src/com/android/i18n/addressinput/CacheData.java
index 3a32b17..db02e07 100644
--- a/src/com/android/i18n/addressinput/CacheData.java
+++ b/src/com/android/i18n/addressinput/CacheData.java
@@ -25,8 +25,10 @@
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
 import java.net.HttpURLConnection;
 import java.net.URL;
+import java.net.URLEncoder;
 import java.util.EventListener;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -249,7 +251,13 @@
     jsonp.setTimeout(TIMEOUT);
     final JsonHandler handler = new JsonHandler(key.toString(),
         existingJso, listener);
-    jsonp.requestObject(serviceUrl + "/" + key.toString(),
+    String keyString;
+    try {
+      keyString = URLEncoder.encode(key.toString(), "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+    jsonp.requestObject(serviceUrl + "/" + keyString,
         new AsyncCallback<JsoMap>() {
           public void onFailure(Throwable caught) {
             Log.w(TAG, "Request for key " + key + " failed");
diff --git a/src/com/android/i18n/addressinput/JsoMap.java b/src/com/android/i18n/addressinput/JsoMap.java
index 9a60766..c756801 100644
--- a/src/com/android/i18n/addressinput/JsoMap.java
+++ b/src/com/android/i18n/addressinput/JsoMap.java
@@ -21,6 +21,9 @@
 import org.json.JSONObject;
 import org.json.JSONTokener;
 
+import java.util.ArrayList;
+import java.util.Iterator;
+
 /**
  * Compatibility methods on top of the JSON data.
  *
@@ -113,10 +116,21 @@
    * @param key key name.
    * @return JsoMap object.
    */
+  @SuppressWarnings("unchecked")  // JSONObject.keys() has no type information.
   public JsoMap getObj(String key) {
-    String[] names = { key };
     try {
-      return new JsoMap(this, names);
+      Object o = super.get(key);
+      if (o instanceof JSONObject) {
+        JSONObject value = (JSONObject)o;
+        ArrayList<String> keys = new ArrayList<String>(value.length());
+        for (Iterator<String> it = value.keys(); it.hasNext(); ) {
+          keys.add(it.next());
+        }
+        String[] names = new String[keys.size()];
+        return new JsoMap(value, keys.toArray(names));        
+      } else {
+        return null;
+      }
     } catch (JSONException e) {
       return null;
     }
diff --git a/test/com/android/i18n/addressinput/CacheDataTest.java b/test/com/android/i18n/addressinput/CacheDataTest.java
new file mode 100644
index 0000000..abe778e
--- /dev/null
+++ b/test/com/android/i18n/addressinput/CacheDataTest.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2010 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.android.i18n.addressinput;
+
+import junit.framework.TestCase;
+
+public class CacheDataTest extends TestCase {
+  private CacheData cache;
+
+  private static final String DELIM = "~";
+
+  private static final String CANADA_KEY = "data/CA";
+
+  private static final String US_KEY = "data/US";
+
+  private static final String CALIFORNIA_KEY = "data/US/CA";
+
+  private static final String RANDOM_COUNTRY_KEY = "data/asIOSDxcowW";
+
+  private static final String EXAMPLE_LOCAL_US_KEY = "examples/US/local"
+      + "/_default";
+
+  // Data key for Da-an District, Taipei Taiwan
+  private static final String TW_KEY = "data/TW/\u53F0\u5317\u5E02/\u5927"
+      + "\u5B89\u5340";
+
+  private static final String FRANCE_KEY = "data/FR";
+
+  private static Integer listenerInvokeCount = 0;
+
+  private static boolean reachedMaxCount = false;
+
+  public void setUp() {
+    cache = new CacheData();
+  }
+
+  public void testSimpleFetching() {
+    final LookupKey key = new LookupKey.Builder(CANADA_KEY).build();
+
+    delayTestFinish(10000);
+
+    cache.fetchDynamicData(key, null, new DataLoadListener() {
+      boolean beginCalled = false;
+
+      public void dataLoadingBegin() {
+        beginCalled = true;
+      }
+
+      public void dataLoadingEnd() {
+        assertTrue("dataLoadingBegin should be called", beginCalled);
+        JsoMap map = cache.getObj(CANADA_KEY);
+
+        assertTrue(map.containsKey(AddressDataKey.ID.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.NAME.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.LANG.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.UPPER.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.ZIPEX.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.ZIP.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.FMT.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.LANGUAGES.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.SUB_KEYS.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.SUB_NAMES.name()
+            .toLowerCase()));
+        assertFalse(map.containsKey(AddressDataKey.SUB_LNAMES.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.SUB_ZIPS.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.POSTURL.name()
+            .toLowerCase()));
+
+        int namesSize = map.get(AddressDataKey.SUB_NAMES.name()
+            .toLowerCase()).split(DELIM).length;
+        int keysSize = map.get(AddressDataKey.SUB_KEYS.name()
+            .toLowerCase()).split(DELIM).length;
+
+        assertEquals("Expect 13 states in Canada.", 13, namesSize);
+        assertEquals(namesSize, keysSize);
+        finishTest();
+      }
+    });
+  }
+
+  public void testFetchingTaiwanData() {
+    final LookupKey key = new LookupKey.Builder(TW_KEY).build();
+
+    delayTestFinish(10000);
+
+    cache.fetchDynamicData(key, null, new DataLoadListener() {
+      boolean beginCalled = false;
+
+      public void dataLoadingBegin() {
+        beginCalled = true;
+      }
+
+      public void dataLoadingEnd() {
+        assertTrue("dataLoadingBegin should be called", beginCalled);
+
+        JsoMap map = cache.getObj(TW_KEY);
+
+        assertTrue(map.containsKey(AddressDataKey.ID.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.KEY.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.LNAME.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.LANG.name()
+            .toLowerCase()));
+        assertTrue(map.containsKey(AddressDataKey.ZIP.name()
+            .toLowerCase()));
+        assertFalse(map.containsKey(AddressDataKey.FMT.name()
+            .toLowerCase()));
+        assertFalse(map.containsKey(AddressDataKey.SUB_KEYS.name()
+            .toLowerCase()));
+        assertFalse(map.containsKey(AddressDataKey.SUB_NAMES.name()
+            .toLowerCase()));
+        assertFalse(map.containsKey(AddressDataKey.SUB_LNAMES.name()
+            .toLowerCase()));
+        assertFalse(map.containsKey(AddressDataKey.SUB_ZIPS.name()
+            .toLowerCase()));
+
+        // Da-an district.
+        assertEquals("\u5927\u5B89\u5340", map.get(AddressDataKey.KEY.name()
+            .toLowerCase()));
+
+        assertEquals("zh-hant", map.get(AddressDataKey.LANG.name()
+            .toLowerCase()));
+
+        finishTest();
+      }
+    });
+  }
+
+  public void testFetchingExamples() {
+    final LookupKey key = new LookupKey.Builder(EXAMPLE_LOCAL_US_KEY).build();
+
+    delayTestFinish(10000);
+
+    cache.fetchDynamicData(key, null, new DataLoadListener() {
+      boolean beginCalled = false;
+
+      public void dataLoadingBegin() {
+        beginCalled = true;
+      }
+
+      public void dataLoadingEnd() {
+        assertTrue("dataLoadingBegin should be called", beginCalled);
+
+        JsoMap map = cache.getObj(EXAMPLE_LOCAL_US_KEY);
+        assertTrue(map.containsKey(AddressDataKey.NAME.name().toLowerCase()));
+        finishTest();
+      }
+    });
+  }
+
+  public void testFetchingOneKeyManyTimes() {
+    final LookupKey key = new LookupKey.Builder(CALIFORNIA_KEY).build();
+    final int maxCount = 10;
+
+    class CounterListener implements DataLoadListener {
+      public void dataLoadingBegin() {
+        listenerInvokeCount++;
+        if (listenerInvokeCount == maxCount) {
+          reachedMaxCount = true;
+        }
+        assertTrue(
+            "CounterListener's dataLoadingBegin should not be invoked for more "
+            + "than " + maxCount + " times",
+            listenerInvokeCount <= maxCount);
+      }
+
+      public void dataLoadingEnd() {
+        listenerInvokeCount--;
+        assertTrue(listenerInvokeCount >= 0);
+        if (listenerInvokeCount == 0) {
+          assertTrue(
+              "Expect to see key " + key + " cached when CounterListener's "
+              + " dataLoadingEnd is invoked",
+              cache.containsKey(key.toString()));
+          /* TODO: Un-comment when CacheData is updated to be asynchronous.
+          assertTrue(
+              "Expect CounterListener's dataLoadingEnd to be triggered "
+              + maxCount + " times in total",
+              reachedMaxCount);
+          */
+          finishTest();
+        }
+      }
+    }
+
+    delayTestFinish(10000);
+
+    for (int i = 0; i < maxCount; ++i) {
+      cache.fetchDynamicData(key, null, new CounterListener());
+    }
+
+    // Null listeners should not affect results.
+    cache.fetchDynamicData(key, null, null);
+    cache.fetchDynamicData(key, null, null);
+    cache.fetchDynamicData(key, null, null);
+  }
+
+  public void testFetchAgainRighAfterOneFetchStart() {
+    final LookupKey key = new LookupKey.Builder(US_KEY).build();
+
+    delayTestFinish(10000);
+
+    cache.fetchDynamicData(key, null, null);
+
+    cache.fetchDynamicData(key, null, new DataLoadListener() {
+      boolean beginCalled = false;
+
+      public void dataLoadingBegin() {
+        /* TODO: Un-comment when CacheData is updated to be asynchronous.
+        assertFalse("data for key " + key + " should not be fetched yet",
+            cache.containsKey(key.toString()));
+        */
+        beginCalled = true;
+      }
+
+      public void dataLoadingEnd() {
+        assertTrue("dataLoadingBegin should be called", beginCalled);
+
+        assertTrue(cache.containsKey(key.toString()));
+
+        cache.fetchDynamicData(key, null, new DataLoadListener() {
+          boolean beginCalled2 = false;
+
+          public void dataLoadingBegin() {
+            beginCalled2 = true;
+          }
+
+          public void dataLoadingEnd() {
+            assertTrue("dataLoadingBegin should be called", beginCalled2);
+
+            assertTrue(cache.containsKey(key.toString()));
+            finishTest();
+          }
+        });
+      }
+    });
+  }
+
+  public void testInvalidKey() {
+    final LookupKey key = new LookupKey.Builder(RANDOM_COUNTRY_KEY).build();
+
+    delayTestFinish(15000);
+
+    cache.fetchDynamicData(key, null, new DataLoadListener() {
+      boolean beginCalled = false;
+
+      public void dataLoadingBegin() {
+        beginCalled = true;
+      }
+
+      public void dataLoadingEnd() {
+        assertTrue("dataLoadingBegin should be called", beginCalled);
+        assertFalse(cache.containsKey(key.toString()));
+
+        finishTest();
+      }
+    });
+  }
+
+  public void testSetUrl() {
+    final LookupKey key = new LookupKey.Builder(FRANCE_KEY).build();
+    final String originalUrl = cache.getUrl();
+
+    assertFalse(FRANCE_KEY + " should not be in the cache. Do you request "
+        + " it before this test?", cache.containsKey(key.toString()));
+
+    delayTestFinish(10000);
+    // Something that is not an URL.
+    cache.setUrl("FDSSfdfdsfasdfadsf");
+
+    cache.fetchDynamicData(key, null, new DataLoadListener() {
+      boolean beginCalled = false;
+
+      public void dataLoadingBegin() {
+        beginCalled = true;
+      }
+
+      public void dataLoadingEnd() {
+        assertTrue("dataLoadingBegin should be called", beginCalled);
+        assertFalse(cache.containsKey(key.toString()));
+        cache.setUrl(originalUrl);
+        finishTest();
+      }
+    });
+  }
+
+  //
+  // Temporary implementations of things that the GWT implementation depends on.
+  //
+  // TODO: Write real implementations and remove these.
+  //
+
+  // To be used when CacheData is updated to be asynchronous.
+  private static void delayTestFinish(int timeoutMillis) {
+  }
+
+  // To be used when CacheData is updated to be asynchronous.
+  private static void finishTest() {
+  }
+}