Port of LookupKey and changing compilation to include line numbers for unit-test debugging.
diff --git a/build.xml b/build.xml
index 9cf021f..bb56f51 100644
--- a/build.xml
+++ b/build.xml
@@ -23,8 +23,12 @@
 
   <target name="compile" description="Compile Java source.">
     <mkdir dir="${classes.dir}"/>
-    <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"/>
-    <javac srcdir="${test.dir}" destdir="${classes.dir}" classpathref="test.classpath"/>
+    <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"
+      debug="on">
+      <compilerarg value="-Xlint"/>
+    </javac>
+    <javac srcdir="${test.dir}" destdir="${classes.dir}" classpathref="test.classpath"
+      debug="on"/>
   </target>
 
   <target name="jar" depends="compile">
diff --git a/src/com/android/i18n/addressinput/LookupKey.java b/src/com/android/i18n/addressinput/LookupKey.java
new file mode 100755
index 0000000..129c90c
--- /dev/null
+++ b/src/com/android/i18n/addressinput/LookupKey.java
@@ -0,0 +1,413 @@
+/*
+ * 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 java.util.EnumMap;
+import java.util.Map;
+
+/**
+ * A builder for creating keys that are used to lookup data in the local cache
+ * and fetch data from the server. There are two key types: {@code KeyType#DATA}
+ * or {@code KeyType#EXAMPLES}.
+ *
+ * <p>
+ * The {@code KeyType#DATA} key is built based on a universal Address hierarchy,
+ * which is:<br>
+ *
+ * {@code AddressField#Country} -> {@code AddressField#ADMIN_AREA} ->
+ * {@code AddressField#Locality} -> {@code AddressField#DEPENDENT_LOCALITY}
+ * </p>
+ *
+ * <p>
+ * The {@code KeyType#EXAMPLES} key is built with the following format:<br>
+ *
+ * {@code AddressField#Country} -> {@code ScriptType} -> language.
+ * </p>
+ */
+public final class LookupKey {
+
+  /**
+   * Key types. Address Widget organizes address info based on key types. For
+   * example, if you want to know how to verify or format an US address, you
+   * need to use {@link KeyType#DATA} to get that info; if you want to get an
+   * example address, you use {@link KeyType#EXAMPLES} instead.
+   */
+  public enum KeyType {
+    /** Key type for getting address data. */
+    DATA,
+    /** Key type for getting examples. */
+    EXAMPLES
+  }
+
+  /**
+   * Script types. This is used for countries that do not use Latin script,
+   * but accept it for transcribing their addresses. For example, you can
+   * write a Japanese address in Latin script instead of Japanese:
+   *
+   * <p>
+   * 7-2, Marunouchi 2-Chome, Chiyoda-ku, Tokyo 100-8799
+   * </p>
+   *
+   * Notice that {@link ScriptType} is based on country/region, not language.
+   */
+  public enum ScriptType {
+    /**
+     * The script that uses Roman characters like ABC (as opposed to scripts
+     * like Cyrillic or Arabic).
+     */
+    LATIN,
+
+    /**
+     * Local scripts. For Japan, it's Japanese (including Hiragana, Katagana,
+     * and Kanji); For Saudi Arabia, it's Arabic. Notice that for US, the local
+     * script is actually Latin script (The same goes for other countries
+     * that use Latin script). For these countries, we do not provide two
+     * set of data (Latin and local) since they use only Latin script. You
+     * have to specify the {@link ScriptType} as local instead Latin.
+     */
+    LOCAL
+  }
+
+  /**
+   * The universal address hierarchy. Notice that sub-administrative
+   * area is neglected here since it is not required to fill out address
+   * form.
+   */
+  private static AddressField[] hierarchy = {
+      AddressField.COUNTRY,
+      AddressField.ADMIN_AREA,
+      AddressField.LOCALITY,
+      AddressField.DEPENDENT_LOCALITY};
+
+  private static final String SLASH_DELIM = "/";
+  private static final String DASH_DELIM = "--";
+  private static final String DEFAULT_LANGUAGE = "_default";
+
+  private final KeyType keyType;
+
+  private final ScriptType scriptType;
+
+  // Values for hierarchy address fields.
+  private final Map<AddressField, String> nodes;
+
+  private final String keyString;
+
+  private final String languageCode;
+
+  private LookupKey(Builder builder) {
+    this.keyType = builder.keyType;
+    this.scriptType = builder.script;
+    this.nodes = builder.nodes;
+    this.languageCode = builder.languageCode;
+    this.keyString = getKeyString();
+  }
+
+  /**
+   * Gets lookup key for the input address field. This method does not allow key
+   * with key type of {@link KeyType#EXAMPLES}.
+   *
+   * @param field a field in the address hierarchy.
+   * @return key of the specified address field. If address field is not in the
+   * hierarchy, or is more granular than the current key has, returns null. For
+   * example, if your current key is "data/US" (down to country level), and you
+   * wants to get the key for Locality (more granular than country), it will
+   * return null.
+   *
+   */
+  public LookupKey getKeyForUpperLevelField(AddressField field) {
+    if (keyType != KeyType.DATA) {
+      // We only support getting the parent key for the data key type.
+      throw new RuntimeException("Only support getting parent keys for the data key type.");
+    }
+    Builder newKeyBuilder = new Builder(this);
+
+    boolean removeNode = false;
+    boolean fieldInHierarchy = false;
+    for (AddressField hierarchyField : hierarchy) {
+      if (removeNode) {
+        if (newKeyBuilder.nodes.containsKey(hierarchyField)) {
+          newKeyBuilder.nodes.remove(hierarchyField);
+        }
+      }
+      if (hierarchyField == field) {
+        if (!newKeyBuilder.nodes.containsKey(hierarchyField)) {
+          return null;
+        }
+        removeNode = true;
+        fieldInHierarchy = true;
+      }
+    }
+
+    if (!fieldInHierarchy) {
+      return null;
+    }
+
+    newKeyBuilder.languageCode = languageCode;
+    newKeyBuilder.script = scriptType;
+
+    return newKeyBuilder.build();
+  }
+
+  /**
+   * Gets parent key for data key. For example, parent key for "data/US/CA" is
+   * "data/US". This method does not allow key with key type of
+   * {@link KeyType#EXAMPLES}.
+   */
+  public LookupKey getParentKey() {
+    if (keyType != KeyType.DATA) {
+      throw new RuntimeException("Only support getting parent keys for the data key type.");
+    }
+    // Root key's parent should be null.
+    if (!nodes.containsKey(AddressField.COUNTRY)) {
+      return null;
+    }
+
+    Builder parentKeyBuilder = new Builder(this);
+    AddressField mostGranularField = AddressField.COUNTRY;
+
+    for (AddressField hierarchyField : hierarchy) {
+      if (!nodes.containsKey(hierarchyField)) {
+        break;
+      }
+      mostGranularField = hierarchyField;
+    }
+    parentKeyBuilder.nodes.remove(mostGranularField);
+    return parentKeyBuilder.build();
+  }
+
+  public KeyType getKeyType() {
+    return keyType;
+  }
+
+  /**
+   * Gets a key in string format. E.g., "data/US/CA".
+   */
+  private String getKeyString() {
+    StringBuilder keyBuilder = new StringBuilder(keyType.name().toLowerCase());
+
+    if (keyType == KeyType.DATA) {
+      for (AddressField field : hierarchy) {
+        if (!nodes.containsKey(field)) {
+          break;
+        }
+        if (field == AddressField.COUNTRY && languageCode != null) {
+          keyBuilder.append(SLASH_DELIM)
+              .append(nodes.get(field)).append(DASH_DELIM)
+              .append(languageCode);
+        } else {
+          keyBuilder.append(SLASH_DELIM).append(nodes.get(field));
+        }
+      }
+    } else {
+      if (nodes.containsKey(AddressField.COUNTRY)) {
+        // Example key. E.g., "examples/TW/local/_default".
+        keyBuilder.append(SLASH_DELIM).append(nodes.get(AddressField.COUNTRY))
+            .append(SLASH_DELIM).append(scriptType.name().toLowerCase())
+            .append(SLASH_DELIM).append(DEFAULT_LANGUAGE);
+      }
+    }
+
+    return keyBuilder.toString();
+  }
+
+  /**
+   * Gets a lookup key as a plain text string., e.g., "data/US/CA".
+   */
+  public String toString() {
+    return keyString;
+  }
+
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if ((obj == null) || (obj.getClass() != this.getClass())) {
+      return false;
+    }
+
+    return ((LookupKey) obj).toString().equals(keyString);
+  }
+
+  public int hashCode() {
+    return keyString.hashCode();
+  }
+
+  /**
+   * Builds lookup keys.
+   */
+  public static class Builder {
+    private KeyType keyType;
+
+    // Default to LOCAL script.
+    private ScriptType script = ScriptType.LOCAL;
+
+    private Map<AddressField, String> nodes = new EnumMap<AddressField, String>(AddressField.class);
+
+    private String languageCode;
+
+    /**
+     * Creates a new builder for the specified key type. keyType cannot be null.
+     */
+    public Builder(KeyType keyType) {
+      this.keyType = keyType;
+    }
+
+    /**
+     * Creates a new builder for the specified key. oldKey cannot be null.
+     */
+    public Builder(LookupKey oldKey) {
+      this.keyType = oldKey.keyType;
+      this.script = oldKey.scriptType;
+      this.languageCode = oldKey.languageCode;
+      for (AddressField field : hierarchy) {
+        if (!oldKey.nodes.containsKey(field)) {
+          break;
+        }
+        this.nodes.put(field, oldKey.nodes.get(field));
+      }
+    }
+
+    /**
+     * Builds the {@link LookupKey} with the input key string. Input
+     * string has to represent either a {@link KeyType#DATA} key or a
+     * {@link KeyType#EXAMPLES} key. Also, key hierarchy deeper than
+     * {@link AddressField#DEPENDENT_LOCALITY} is not allowed. Notice that if
+     * any node in the hierarchy is empty, all the descendant nodes' values will
+     * be neglected. For example, input string "data/US//Mt View" will become
+     * "data/US".
+     *
+     * @param keyString e.g., "data/US/CA"
+     */
+    public Builder(String keyString) {
+      String[] parts = keyString.split(SLASH_DELIM);
+      // Check some pre-conditions.
+      if (!parts[0].equals(KeyType.DATA.name().toLowerCase()) &&
+          !parts[0].equals(KeyType.EXAMPLES.name().toLowerCase())) {
+        throw new RuntimeException("Wrong key type: " + parts[0]);
+      }
+      if (parts.length > hierarchy.length + 1) {
+        throw new RuntimeException("input key '" + keyString + "' deeper than supported hierarchy");
+      }
+      if (parts[0].equals("data")) {
+        keyType = KeyType.DATA;
+
+        // Parses country and language info.
+        if (parts.length > 1) {
+          String substr = Util.trimToNull(parts[1]);
+          if (substr.contains(DASH_DELIM)) {
+            String[] s = substr.split(DASH_DELIM);
+            if (s.length != 2) {
+              throw new RuntimeException(
+                  "Wrong format: Substring should be country code--language code");
+            }
+            substr = s[0];
+            languageCode = s[1];
+          }
+          this.nodes.put(hierarchy[0], substr);
+        }
+
+        // Parses sub-country info.
+        if (parts.length > 2) {
+          for (int i = 2; i < parts.length; ++i) {
+            String substr = Util.trimToNull(parts[i]);
+            if (substr == null) {
+              break;
+            }
+            this.nodes.put(hierarchy[i - 1], substr);
+          }
+        }
+      } else if (parts[0].equals("examples")) {
+        keyType = KeyType.EXAMPLES;
+
+        // Parses country info.
+        if (parts.length > 1) {
+          this.nodes.put(AddressField.COUNTRY, parts[1]);
+        }
+
+        // Parses script types.
+        if (parts.length > 2) {
+          String scriptStr = parts[2];
+          if (scriptStr.equals("local")) {
+            this.script = ScriptType.LOCAL;
+          } else if (scriptStr.equals("latin")) {
+            this.script = ScriptType.LATIN;
+          } else {
+            throw new RuntimeException("Script type has to be either latin or local.");
+          }
+        }
+
+        // Parses language code. Example: "zh_Hant" in
+        // "examples/TW/local/zH_Hant".
+        if (parts.length > 3 && !parts[3].equals(DEFAULT_LANGUAGE)) {
+          languageCode = parts[3];
+        }
+      }
+    }
+
+    public Builder setLanguageCode(String languageCode) {
+      this.languageCode = languageCode;
+      return this;
+    }
+
+    /**
+     * Sets key using {@link AddressData}. Notice that if any node in the
+     * hierarchy is empty, all the descendant nodes' values will
+     * be neglected. For example, the following address misses
+     * {@link AddressField#ADMIN_AREA}, thus its data key will be
+     * "data/US".
+     *
+     * <p>
+     * country: US<br>
+     * administrative area: null<br>
+     * locality: Mt. View
+     * </p>
+     */
+    public Builder setAddressData(AddressData data) {
+      languageCode = data.getLanguageCode();
+      if (languageCode != null) {
+        if (Util.isExplicitLatinScript(languageCode)) {
+          script = ScriptType.LATIN;
+        }
+      }
+
+      if (data.getPostalCountry() == null) {
+        return this;
+      }
+      this.nodes.put(AddressField.COUNTRY, data.getPostalCountry());
+
+      if (data.getAdministrativeArea() == null) {
+        return this;
+      }
+      this.nodes.put(AddressField.ADMIN_AREA, data.getAdministrativeArea());
+
+      if (data.getLocality() == null) {
+        return this;
+      }
+      this.nodes.put(AddressField.LOCALITY, data.getLocality());
+
+      if (data.getDependentLocality() == null) {
+        return this;
+      }
+      this.nodes.put(AddressField.DEPENDENT_LOCALITY, data.getDependentLocality());
+      return this;
+    }
+
+    public LookupKey build() {
+      return new LookupKey(this);
+    }
+  }
+}
diff --git a/src/com/android/i18n/addressinput/Util.java b/src/com/android/i18n/addressinput/Util.java
index 081e973..22620d7 100644
--- a/src/com/android/i18n/addressinput/Util.java
+++ b/src/com/android/i18n/addressinput/Util.java
@@ -21,34 +21,19 @@
 
 /**
  * Utility functions used by the address widget.
- *
- * @author Lara Rennie
  */
 public class Util {
-  public static final String LATIN_SCRIPT = "Latn";
-
-  // Only used internally.
-  private static final String CHINESE_SCRIPT = "Hans";
-  private static final String KOREAN_SCRIPT = "Kore";
-  private static final String JAPANESE_SCRIPT = "Jpan";
-  // These are in upper-case, since we convert the language code to upper case before doing
-  // string comparison.
-  private static final String CHINESE_LANGUAGE = "ZH";
-  private static final String JAPANESE_LANGUAGE = "JA";
-  private static final String KOREAN_LANGUAGE = "KO";
+  // In upper-case, since we convert the language code to upper case before doing string comparison.
+  private static final String LATIN_SCRIPT = "LATN";
 
   // Cannot instantiate this class - private constructor.
   private Util() {}
 
   /**
-   * Gets the script code for a particular language. This is a somewhat hacky replacement for ICU's
-   * class that does this properly. For our purposes, we only want to know if the address is in a
-   * CJK script or not, since that affects address formatting. We assume that the languageCode is
-   * well-formed and first search to see if there is a script code specified. If not, then we assume
-   * Chinese, Japanese and Korean are in their default scripts, and other languages are in Latin
-   * script.
+   * Returns true if the language code is explicitly marked to be in the latin script. For example,
+   * "zh-Latn" would return true, but "zh-TW", "en" and "zh" would all return false.
    */
-  public static String getScriptCode(String languageCode) {
+  public static boolean isExplicitLatinScript(String languageCode) {
     // Convert to upper-case for easier comparison.
     languageCode = languageCode.toUpperCase();
     // Check to see if the language code contains a script modifier.
@@ -56,23 +41,22 @@
     Matcher m = languageCodePattern.matcher(languageCode);
     if (m.lookingAt()) {
       String script = m.group(1);
-      if (script.equals(LATIN_SCRIPT.toUpperCase())) {
-        return LATIN_SCRIPT;
+      if (script.equals(LATIN_SCRIPT)) {
+        return true;
       }
     }
-    // If the script was not explicitly specified as Latn, we ignore the script information and read
-    // the language tag instead. This would break for cases such as zh-Cyrl, but this is rare enough
-    // that we are not going to worry about it for now.
-    if (languageCode.startsWith(CHINESE_LANGUAGE)) {
-      // We don't distinguish between simplified and traditional Chinese here.
-      return CHINESE_SCRIPT;
-    } else if (languageCode.startsWith(JAPANESE_LANGUAGE)) {
-      return JAPANESE_SCRIPT;
-    } else if (languageCode.startsWith(KOREAN_LANGUAGE)) {
-      return KOREAN_SCRIPT;
+    return false;
+  }
+
+  /**
+   * Trims the string. If the field is empty after trimming, returns null instead. Note that this
+   * only trims ASCII white-space.
+   */
+  public static String trimToNull(String originalStr) {
+    if (originalStr == null) {
+      return null;
     }
-    // All Indic, Arabic and other scripts will be mislabelled by this function, but since we only
-    // want to distinguish between CJK and non-CJK, this is ok.
-    return LATIN_SCRIPT;
+    String trimmedString = originalStr.trim();
+    return (trimmedString.length() == 0) ? null : trimmedString;
   }
 }
diff --git a/test/com/android/i18n/addressinput/LookupKeyTest.java b/test/com/android/i18n/addressinput/LookupKeyTest.java
new file mode 100644
index 0000000..7dd77a8
--- /dev/null
+++ b/test/com/android/i18n/addressinput/LookupKeyTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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 com.android.i18n.addressinput.LookupKey.KeyType;
+
+import junit.framework.TestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for the LookupKey class.
+ */
+public class LookupKeyTest extends TestCase {
+  private static final String ROOT_KEY = "data";
+  private static final String ROOT_EXAMPLE_KEY = "examples";
+  private static final String US_KEY = "data/US";
+  private static final String CALIFORNIA_KEY = "data/US/CA";
+  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";
+
+  // Example key for TW's address (local script)
+  private static final String TW_EXAMPLE_LOCAL_KEY = "examples/TW/local/_default";
+
+  // Example key for TW's address (latin script)
+  private static final String TW_EXAMPLE_LATIN_KEY = "examples/TW/latin/_default";
+
+  private static final String RANDOM_KEY = "sdfIisooIFOOBAR";
+  private static final String RANDOM_COUNTRY_KEY = "data/asIOSDxcowW";
+
+  public void testRootKey() {
+    LookupKey key = new LookupKey.Builder(KeyType.DATA).build();
+    assertEquals(ROOT_KEY, key.toString());
+
+    LookupKey key2 = new LookupKey.Builder(key.toString()).build();
+    assertEquals(ROOT_KEY, key.toString());
+  }
+
+  public void testDataKeys() {
+    LookupKey key = new LookupKey.Builder(US_KEY).build();
+    assertEquals(US_KEY, key.toString());
+
+    LookupKey key2 = new LookupKey.Builder(CALIFORNIA_KEY).build();
+    assertEquals(CALIFORNIA_KEY, key2.toString());
+  }
+
+  public void testExampleRootKeys() {
+    LookupKey key = new LookupKey.Builder(KeyType.EXAMPLES).build();
+    assertEquals(ROOT_EXAMPLE_KEY, key.toString());
+  }
+
+  public void testExampleKeys() {
+    AddressData address =
+        new AddressData.Builder().setCountry("US").setLanguageCode("en").build();
+
+    LookupKey key = new LookupKey.Builder(KeyType.EXAMPLES).setAddressData(address).build();
+    assertEquals(EXAMPLE_LOCAL_US_KEY, key.toString());
+
+    key = new LookupKey.Builder(EXAMPLE_LOCAL_US_KEY).build();
+    assertEquals(EXAMPLE_LOCAL_US_KEY, key.toString());
+  }
+
+  public void testKeyWithWrongScriptType() {
+    String wrongScript = "examples/US/asdfasdfasdf/_default";
+    try {
+      LookupKey key = new LookupKey.Builder(wrongScript).build();
+      fail("should fail since the script type is wrong");
+    } catch (RuntimeException e) {
+      // Expected.
+    }
+  }
+
+  public void testFallbackToCountry() {
+    // Admin Area is missing.
+    AddressData address = new AddressData.Builder().setCountry("US").setLocality("Mt View").build();
+
+    LookupKey key = new LookupKey.Builder(KeyType.DATA).setAddressData(address).build();
+
+    assertEquals("locality should be omitted since admin area is not specified",
+                 US_KEY, key.toString());
+
+    // Tries key string with the same problem (missing Admin Area).
+    key = new LookupKey.Builder("data/US//Mt View").build();
+
+    assertEquals("locality should be omitted since admin area is not specified",
+                 US_KEY, key.toString());
+  }
+
+  public void testNonUsAddress() {
+    AddressData address = new AddressData.Builder().setCountry("TW")
+        .setAdminArea("\u53F0\u5317\u5E02")  // Taipei city
+        .setLocality("\u5927\u5B89\u5340")  // Da-an district
+        .build();
+
+    LookupKey key = new LookupKey.Builder(KeyType.DATA).setAddressData(address).build();
+    assertEquals(TW_KEY, key.toString());
+
+    key = new LookupKey.Builder(KeyType.EXAMPLES).setAddressData(address).build();
+    assertEquals(TW_EXAMPLE_LOCAL_KEY, key.toString());
+
+    address = new AddressData.Builder(address).setLanguageCode("zh-latn").build();
+    key = new LookupKey.Builder(KeyType.EXAMPLES).setAddressData(address).build();
+    assertEquals(TW_EXAMPLE_LATIN_KEY, key.toString());
+  }
+
+  public void testGetKeyForUpperLevelFieldWithDataKey() {
+    AddressData address = new AddressData.Builder().setCountry("US")
+        .setAdminArea("CA")
+        .setLocality("Mt View")
+        .build();
+
+    LookupKey key = new LookupKey.Builder(KeyType.DATA).setAddressData(address).build();
+    LookupKey newKey = key.getKeyForUpperLevelField(AddressField.COUNTRY);
+    assertNotNull("failed to get key for " + AddressField.COUNTRY, newKey);
+    assertEquals("data/US", newKey.toString());
+
+    newKey = key.getKeyForUpperLevelField(AddressField.ADMIN_AREA);
+    assertNotNull("failed to get key for " + AddressField.ADMIN_AREA, newKey);
+    assertEquals("data/US/CA", newKey.toString());
+    assertEquals("original key should not be changed", "data/US/CA/Mt View",
+        key.toString());
+
+    newKey = key.getKeyForUpperLevelField(AddressField.LOCALITY);
+    assertNotNull("failed to get key for " + AddressField.LOCALITY, newKey);
+    assertEquals("data/US/CA/Mt View", newKey.toString());
+
+    newKey = key.getKeyForUpperLevelField(AddressField.DEPENDENT_LOCALITY);
+    assertNull("should return null for field not contained in current key", newKey);
+
+    newKey = key.getKeyForUpperLevelField(AddressField.RECIPIENT);
+    assertNull("should return null since field '" + AddressField.RECIPIENT
+        + "' is not in address hierarchy", newKey);
+  }
+
+  public void testGetKeyForUpperLevelFieldWithExampleKey() {
+    LookupKey key = new LookupKey.Builder("examples/US/latin/_default").build();
+
+    try {
+      LookupKey newKey = key.getKeyForUpperLevelField(AddressField.COUNTRY);
+      fail("should fail if you try to get parent key for an example key.");
+    } catch (RuntimeException e) {
+      // Expected.
+    }
+  }
+
+  public void testGetParentKey() {
+    AddressData address = new AddressData.Builder().setCountry("US")
+        .setAdminArea("CA")
+        .setLocality("Mt View")
+        .setDependentLocality("El Camino")
+        .build();
+
+    LookupKey key = new LookupKey.Builder(KeyType.DATA).setAddressData(address).build();
+    assertEquals("data/US/CA/Mt View/El Camino", key.toString());
+
+    key = key.getParentKey();
+    assertEquals("data/US/CA/Mt View", key.toString());
+
+    key = key.getParentKey();
+    assertEquals("data/US/CA", key.toString());
+
+    key = key.getParentKey();
+    assertEquals("data/US", key.toString());
+
+    key = key.getParentKey();
+    assertEquals("data", key.toString());
+
+    key = key.getParentKey();
+    assertNull("root key's parent should be null", key);
+  }
+
+  public void testInvalidKeyTypeWillFail() {
+    try {
+      new LookupKey.Builder(RANDOM_KEY).build();
+      fail("should fail if key string does not start with a valid key type");
+    } catch (RuntimeException e) {
+      // Expected.
+    }
+  }
+
+  /**
+   *  Ensures that even when the input key string is random, we still create a
+   *  key. (We do not verify if the key maps to an real world entity like a city
+   *  or country).
+   */
+  public void testWeDontVerifyKeyName() {
+    LookupKey key = new LookupKey.Builder(RANDOM_COUNTRY_KEY).build();
+    assertEquals(RANDOM_COUNTRY_KEY, key.toString());
+  }
+
+  public void testHash() {
+    String keys[] = { ROOT_KEY, ROOT_EXAMPLE_KEY, US_KEY, CALIFORNIA_KEY };
+    Map<LookupKey, String> map = new HashMap<LookupKey, String>();
+
+    for (String key : keys) {
+      map.put(new LookupKey.Builder(key).build(), key);
+    }
+
+    for (String key : keys) {
+      assertTrue(map.containsKey(new LookupKey.Builder(key).build()));
+      assertEquals(key, map.get(new LookupKey.Builder(key).build()));
+    }
+    assertFalse(map.containsKey(new LookupKey.Builder(RANDOM_COUNTRY_KEY).build()));
+  }
+}
diff --git a/test/com/android/i18n/addressinput/UtilTest.java b/test/com/android/i18n/addressinput/UtilTest.java
index 7cf1843..66366d3 100644
--- a/test/com/android/i18n/addressinput/UtilTest.java
+++ b/test/com/android/i18n/addressinput/UtilTest.java
@@ -19,33 +19,40 @@
 import junit.framework.TestCase;
 
 /**
- * @author Lara Rennie
+ * Tests for util functions.
  */
 public class UtilTest extends TestCase {
 
-  public void testGetScriptCodeLatinScript() throws Exception {
+  public void testIsExplicitLatinScript() throws Exception {
     // Should recognise latin script in a variety of forms.
-    assertEquals(Util.LATIN_SCRIPT, Util.getScriptCode("en"));
-    assertEquals(Util.LATIN_SCRIPT, Util.getScriptCode("EN"));
-    assertEquals(Util.LATIN_SCRIPT, Util.getScriptCode("zh-Latn"));
-    assertEquals(Util.LATIN_SCRIPT, Util.getScriptCode("ja_LATN"));
-    assertEquals(Util.LATIN_SCRIPT, Util.getScriptCode("ja_LATN-JP"));
-    assertEquals(Util.LATIN_SCRIPT, Util.getScriptCode("ko-latn_JP"));
-    // Other non-CJK scripts are also labelled as Latin.
-    assertEquals(Util.LATIN_SCRIPT, Util.getScriptCode("ru"));
+    assertTrue(Util.isExplicitLatinScript("zh-Latn"));
+    assertTrue(Util.isExplicitLatinScript("ja_LATN"));
+    assertTrue(Util.isExplicitLatinScript("ja_LATN-JP"));
+    assertTrue(Util.isExplicitLatinScript("ko-latn_JP"));
   }
 
-  public void testGetScriptCodeCjkScript() throws Exception {
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("ko")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("KO")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("ja")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("ja-JP")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("zh-Hans")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("zh-Hans-CN")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("zh-Hant")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("zh-TW")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("zh_TW")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("ko")));
-    assertFalse(Util.LATIN_SCRIPT.equals(Util.getScriptCode("ko_KR")));
+  public void testIsExplicitLatinScriptNonLatin() throws Exception {
+    assertFalse(Util.isExplicitLatinScript("ko"));
+    assertFalse(Util.isExplicitLatinScript("KO"));
+    assertFalse(Util.isExplicitLatinScript("ja"));
+    assertFalse(Util.isExplicitLatinScript("ja-JP"));
+    assertFalse(Util.isExplicitLatinScript("zh-Hans"));
+    assertFalse(Util.isExplicitLatinScript("zh-Hans-CN"));
+    assertFalse(Util.isExplicitLatinScript("zh-Hant"));
+    assertFalse(Util.isExplicitLatinScript("zh-TW"));
+    assertFalse(Util.isExplicitLatinScript("zh_TW"));
+    assertFalse(Util.isExplicitLatinScript("ko"));
+    assertFalse(Util.isExplicitLatinScript("ko_KR"));
+    assertFalse(Util.isExplicitLatinScript("en"));
+    assertFalse(Util.isExplicitLatinScript("EN"));
+    assertFalse(Util.isExplicitLatinScript("ru"));
+  }
+
+  public void testTrimToNull() throws Exception {
+    assertEquals("Trimmed String", Util.trimToNull("  Trimmed String   "));
+    assertEquals("Trimmed String", Util.trimToNull("  Trimmed String"));
+    assertEquals("Trimmed String", Util.trimToNull("Trimmed String"));
+    assertEquals(null, Util.trimToNull("  "));
+    assertEquals(null, Util.trimToNull(null));
   }
 }