Home | History | Annotate | Download | only in addressinput
      1 /*
      2  * Copyright (C) 2010 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.i18n.addressinput;
     18 
     19 import java.util.EnumMap;
     20 import java.util.Map;
     21 
     22 /**
     23  * A builder for creating keys that are used to lookup data in the local cache and fetch data from
     24  * the server. There are two key types: {@code KeyType#DATA} or {@code KeyType#EXAMPLES}.
     25  *
     26  * <p> The {@code KeyType#DATA} key is built based on a universal Address hierarchy, which is:<br>
     27  *
     28  * {@code AddressField#Country} -> {@code AddressField#ADMIN_AREA} -> {@code AddressField#Locality}
     29  * -> {@code AddressField#DEPENDENT_LOCALITY} </p>
     30  *
     31  * <p> The {@code KeyType#EXAMPLES} key is built with the following format:<br>
     32  *
     33  * {@code AddressField#Country} -> {@code ScriptType} -> language. </p>
     34  */
     35 final class LookupKey {
     36 
     37     /**
     38      * Key types. Address Widget organizes address info based on key types. For example, if you want
     39      * to know how to verify or format an US address, you need to use {@link KeyType#DATA} to get
     40      * that info; if you want to get an example address, you use {@link KeyType#EXAMPLES} instead.
     41      */
     42     enum KeyType {
     43 
     44         /**
     45          * Key type for getting address data.
     46          */
     47         DATA,
     48         /**
     49          * Key type for getting examples.
     50          */
     51         EXAMPLES
     52     }
     53 
     54     /**
     55      * Script types. This is used for countries that do not use Latin script, but accept it for
     56      * transcribing their addresses. For example, you can write a Japanese address in Latin script
     57      * instead of Japanese:
     58      *
     59      * <p> 7-2, Marunouchi 2-Chome, Chiyoda-ku, Tokyo 100-8799 </p>
     60      *
     61      * Notice that {@link ScriptType} is based on country/region, not language.
     62      */
     63     enum ScriptType {
     64 
     65         /**
     66          * The script that uses Roman characters like ABC (as opposed to scripts like Cyrillic or
     67          * Arabic).
     68          */
     69         LATIN,
     70 
     71         /**
     72          * Local scripts. For Japan, it's Japanese (including Hiragana, Katagana, and Kanji); For
     73          * Saudi Arabia, it's Arabic. Notice that for US, the local script is actually Latin script
     74          * (The same goes for other countries that use Latin script). For these countries, we do not
     75          * provide two set of data (Latin and local) since they use only Latin script. You have to
     76          * specify the {@link ScriptType} as local instead Latin.
     77          */
     78         LOCAL
     79     }
     80 
     81     /**
     82      * The universal address hierarchy. Notice that sub-administrative area is neglected here since
     83      * it is not required to fill out address form.
     84      */
     85     private static final AddressField[] HIERARCHY = {
     86             AddressField.COUNTRY,
     87             AddressField.ADMIN_AREA,
     88             AddressField.LOCALITY,
     89             AddressField.DEPENDENT_LOCALITY};
     90 
     91     private static final String SLASH_DELIM = "/";
     92 
     93     private static final String DASH_DELIM = "--";
     94 
     95     private static final String DEFAULT_LANGUAGE = "_default";
     96 
     97     private final KeyType mKeyType;
     98 
     99     private final ScriptType mScriptType;
    100 
    101     // Values for hierarchy address fields.
    102     private final Map<AddressField, String> mNodes;
    103 
    104     private final String mKeyString;
    105 
    106     private final String mLanguageCode;
    107 
    108     private LookupKey(Builder builder) {
    109         this.mKeyType = builder.keyType;
    110         this.mScriptType = builder.script;
    111         this.mNodes = builder.nodes;
    112         this.mLanguageCode = builder.languageCode;
    113         this.mKeyString = getKeyString();
    114     }
    115 
    116     /**
    117      * Gets lookup key for the input address field. This method does not allow key with key type of
    118      * {@link KeyType#EXAMPLES}.
    119      *
    120      * @param field a field in the address hierarchy.
    121      * @return key of the specified address field. If address field is not in the hierarchy, or is
    122      *         more granular than the current key has, returns null. For example, if your current
    123      *         key is "data/US" (down to country level), and you want to get the key for Locality
    124      *         (more granular than country), it will return null.
    125      */
    126     LookupKey getKeyForUpperLevelField(AddressField field) {
    127         if (mKeyType != KeyType.DATA) {
    128             // We only support getting the parent key for the data key type.
    129             throw new RuntimeException("Only support getting parent keys for the data key type.");
    130         }
    131         Builder newKeyBuilder = new Builder(this);
    132 
    133         boolean removeNode = false;
    134         boolean fieldInHierarchy = false;
    135         for (AddressField hierarchyField : HIERARCHY) {
    136             if (removeNode) {
    137                 if (newKeyBuilder.nodes.containsKey(hierarchyField)) {
    138                     newKeyBuilder.nodes.remove(hierarchyField);
    139                 }
    140             }
    141             if (hierarchyField == field) {
    142                 if (!newKeyBuilder.nodes.containsKey(hierarchyField)) {
    143                     return null;
    144                 }
    145                 removeNode = true;
    146                 fieldInHierarchy = true;
    147             }
    148         }
    149 
    150         if (!fieldInHierarchy) {
    151             return null;
    152         }
    153 
    154         newKeyBuilder.languageCode = mLanguageCode;
    155         newKeyBuilder.script = mScriptType;
    156 
    157         return newKeyBuilder.build();
    158     }
    159 
    160     /**
    161      * Returns the string value of a field in a key for a particular
    162      * AddressField. For example, for the key "data/US/CA" and the address
    163      * field AddressField.COUNTRY, "US" would be returned. Returns an empty
    164      * string if the key does not have this field in it.
    165      */
    166     String getValueForUpperLevelField(AddressField field) {
    167         // First, get the key for this field.
    168         LookupKey key = getKeyForUpperLevelField(field);
    169         // Now we know the last value in the string is the value for this field.
    170         if (key != null) {
    171             String keyString = key.toString();
    172             int lastSlashPosition = keyString.lastIndexOf(SLASH_DELIM);
    173             if (lastSlashPosition > 0 && lastSlashPosition != keyString.length()) {
    174                 return keyString.substring(lastSlashPosition + 1);
    175             }
    176         }
    177         return "";
    178     }
    179 
    180     /**
    181      * Gets parent key for data key. For example, parent key for "data/US/CA" is "data/US". This
    182      * method does not allow key with key type of {@link KeyType#EXAMPLES}.
    183      */
    184     LookupKey getParentKey() {
    185         if (mKeyType != KeyType.DATA) {
    186             throw new RuntimeException("Only support getting parent keys for the data key type.");
    187         }
    188         // Root key's parent should be null.
    189         if (!mNodes.containsKey(AddressField.COUNTRY)) {
    190             return null;
    191         }
    192 
    193         Builder parentKeyBuilder = new Builder(this);
    194         AddressField mostGranularField = AddressField.COUNTRY;
    195 
    196         for (AddressField hierarchyField : HIERARCHY) {
    197             if (!mNodes.containsKey(hierarchyField)) {
    198                 break;
    199             }
    200             mostGranularField = hierarchyField;
    201         }
    202         parentKeyBuilder.nodes.remove(mostGranularField);
    203         return parentKeyBuilder.build();
    204     }
    205 
    206     KeyType getKeyType() {
    207         return mKeyType;
    208     }
    209 
    210     /**
    211      * Gets a key in string format. E.g., "data/US/CA".
    212      */
    213     private String getKeyString() {
    214         StringBuilder keyBuilder = new StringBuilder(mKeyType.name().toLowerCase());
    215 
    216         if (mKeyType == KeyType.DATA) {
    217             for (AddressField field : HIERARCHY) {
    218                 if (!mNodes.containsKey(field)) {
    219                     break;
    220                 }
    221                 if (field == AddressField.COUNTRY && mLanguageCode != null) {
    222                     keyBuilder.append(SLASH_DELIM)
    223                             .append(mNodes.get(field)).append(DASH_DELIM)
    224                             .append(mLanguageCode);
    225                 } else {
    226                     keyBuilder.append(SLASH_DELIM).append(mNodes.get(field));
    227                 }
    228             }
    229         } else {
    230             if (mNodes.containsKey(AddressField.COUNTRY)) {
    231                 // Example key. E.g., "examples/TW/local/_default".
    232                 keyBuilder.append(SLASH_DELIM).append(mNodes.get(AddressField.COUNTRY))
    233                         .append(SLASH_DELIM).append(mScriptType.name().toLowerCase())
    234                         .append(SLASH_DELIM).append(DEFAULT_LANGUAGE);
    235             }
    236         }
    237 
    238         return keyBuilder.toString();
    239     }
    240 
    241     /**
    242      * Gets a lookup key as a plain text string., e.g., "data/US/CA".
    243      */
    244     @Override
    245     public String toString() {
    246         return mKeyString;
    247     }
    248 
    249     @Override
    250     public boolean equals(Object obj) {
    251         if (this == obj) {
    252             return true;
    253         }
    254         if ((obj == null) || (obj.getClass() != this.getClass())) {
    255             return false;
    256         }
    257 
    258         return ((LookupKey) obj).toString().equals(mKeyString);
    259     }
    260 
    261     @Override
    262     public int hashCode() {
    263         return mKeyString.hashCode();
    264     }
    265 
    266     static boolean hasValidKeyPrefix(String key) {
    267         for (KeyType type : KeyType.values()) {
    268             if (key.startsWith(type.name().toLowerCase())) {
    269                 return true;
    270             }
    271         }
    272         return false;
    273     }
    274 
    275     /**
    276      * Builds lookup keys.
    277      */
    278     static class Builder {
    279 
    280         private KeyType keyType;
    281 
    282         // Default to LOCAL script.
    283 
    284         private ScriptType script = ScriptType.LOCAL;
    285 
    286         private Map<AddressField, String> nodes = new EnumMap<AddressField, String>(
    287                 AddressField.class);
    288 
    289         private String languageCode;
    290 
    291         /**
    292          * Creates a new builder for the specified key type. keyType cannot be null.
    293          */
    294         Builder(KeyType keyType) {
    295             this.keyType = keyType;
    296         }
    297 
    298         /**
    299          * Creates a new builder for the specified key. oldKey cannot be null.
    300          */
    301         Builder(LookupKey oldKey) {
    302             this.keyType = oldKey.mKeyType;
    303             this.script = oldKey.mScriptType;
    304             this.languageCode = oldKey.mLanguageCode;
    305             for (AddressField field : HIERARCHY) {
    306                 if (!oldKey.mNodes.containsKey(field)) {
    307                     break;
    308                 }
    309                 this.nodes.put(field, oldKey.mNodes.get(field));
    310             }
    311         }
    312 
    313         /**
    314          * Builds the {@link LookupKey} with the input key string. Input string has to represent
    315          * either a {@link KeyType#DATA} key or a {@link KeyType#EXAMPLES} key. Also, key hierarchy
    316          * deeper than {@link AddressField#DEPENDENT_LOCALITY} is not allowed. Notice that if any
    317          * node in the hierarchy is empty, all the descendant nodes' values will be neglected. For
    318          * example, input string "data/US//Mt View" will become "data/US".
    319          *
    320          * @param keyString e.g., "data/US/CA"
    321          */
    322         Builder(String keyString) {
    323             String[] parts = keyString.split(SLASH_DELIM);
    324             // Check some pre-conditions.
    325             if (!parts[0].equals(KeyType.DATA.name().toLowerCase()) &&
    326                     !parts[0].equals(KeyType.EXAMPLES.name().toLowerCase())) {
    327                 throw new RuntimeException("Wrong key type: " + parts[0]);
    328             }
    329             if (parts.length > HIERARCHY.length + 1) {
    330                 throw new RuntimeException(
    331                         "input key '" + keyString + "' deeper than supported hierarchy");
    332             }
    333             if (parts[0].equals("data")) {
    334                 keyType = KeyType.DATA;
    335 
    336                 // Parses country and language info.
    337                 if (parts.length > 1) {
    338                     String substr = Util.trimToNull(parts[1]);
    339                     if (substr.contains(DASH_DELIM)) {
    340                         String[] s = substr.split(DASH_DELIM);
    341                         if (s.length != 2) {
    342                             throw new RuntimeException(
    343                                     "Wrong format: Substring should be country "
    344                                             + "code--language code");
    345                         }
    346                         substr = s[0];
    347                         languageCode = s[1];
    348                     }
    349                     this.nodes.put(HIERARCHY[0], substr);
    350                 }
    351 
    352                 // Parses sub-country info.
    353                 if (parts.length > 2) {
    354                     for (int i = 2; i < parts.length; ++i) {
    355                         String substr = Util.trimToNull(parts[i]);
    356                         if (substr == null) {
    357                             break;
    358                         }
    359                         this.nodes.put(HIERARCHY[i - 1], substr);
    360                     }
    361                 }
    362             } else if (parts[0].equals("examples")) {
    363                 keyType = KeyType.EXAMPLES;
    364 
    365                 // Parses country info.
    366                 if (parts.length > 1) {
    367                     this.nodes.put(AddressField.COUNTRY, parts[1]);
    368                 }
    369 
    370                 // Parses script types.
    371                 if (parts.length > 2) {
    372                     String scriptStr = parts[2];
    373                     if (scriptStr.equals("local")) {
    374                         this.script = ScriptType.LOCAL;
    375                     } else if (scriptStr.equals("latin")) {
    376                         this.script = ScriptType.LATIN;
    377                     } else {
    378                         throw new RuntimeException("Script type has to be either latin or local.");
    379                     }
    380                 }
    381 
    382                 // Parses language code. Example: "zh_Hant" in
    383                 // "examples/TW/local/zH_Hant".
    384                 if (parts.length > 3 && !parts[3].equals(DEFAULT_LANGUAGE)) {
    385                     languageCode = parts[3];
    386                 }
    387             }
    388         }
    389 
    390         Builder setLanguageCode(String languageCode) {
    391             this.languageCode = languageCode;
    392             return this;
    393         }
    394 
    395         /**
    396          * Sets key using {@link AddressData}. Notice that if any node in the hierarchy is empty,
    397          * all the descendant nodes' values will be neglected. For example, the following address
    398          * misses {@link AddressField#ADMIN_AREA}, thus its data key will be "data/US".
    399          *
    400          * <p> country: US<br> administrative area: null<br> locality: Mt. View </p>
    401          */
    402         Builder setAddressData(AddressData data) {
    403             languageCode = data.getLanguageCode();
    404             if (languageCode != null) {
    405                 if (Util.isExplicitLatinScript(languageCode)) {
    406                     script = ScriptType.LATIN;
    407                 }
    408             }
    409 
    410             if (data.getPostalCountry() == null) {
    411                 return this;
    412             }
    413             this.nodes.put(AddressField.COUNTRY, data.getPostalCountry());
    414 
    415             if (data.getAdministrativeArea() == null) {
    416                 return this;
    417             }
    418             this.nodes.put(AddressField.ADMIN_AREA, data.getAdministrativeArea());
    419 
    420             if (data.getLocality() == null) {
    421                 return this;
    422             }
    423             this.nodes.put(AddressField.LOCALITY, data.getLocality());
    424 
    425             if (data.getDependentLocality() == null) {
    426                 return this;
    427             }
    428             this.nodes.put(AddressField.DEPENDENT_LOCALITY, data.getDependentLocality());
    429             return this;
    430         }
    431 
    432         LookupKey build() {
    433             return new LookupKey(this);
    434         }
    435     }
    436 }
    437