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 com.android.i18n.addressinput.LookupKey.KeyType;
     20 
     21 import android.util.Log;
     22 
     23 import org.json.JSONArray;
     24 import org.json.JSONException;
     25 
     26 import java.util.EnumMap;
     27 import java.util.HashMap;
     28 import java.util.HashSet;
     29 import java.util.Map;
     30 import java.util.Set;
     31 
     32 /**
     33  * Access point for the cached address verification data. The data contained here will mainly be
     34  * used to build {@link FieldVerifier}'s. This class is implemented as a singleton.
     35  */
     36 public class ClientData implements DataSource {
     37 
     38     private static final String TAG = "ClientData";
     39 
     40     /**
     41      * Data to bootstrap the process. The data are all regional (country level)
     42      * data. Keys are like "data/US/CA"
     43      */
     44     private final Map<String, JsoMap> mBootstrapMap = new HashMap<String, JsoMap>();
     45 
     46     private CacheData mCacheData;
     47 
     48     public ClientData(CacheData cacheData) {
     49         this.mCacheData = cacheData;
     50         buildRegionalData();
     51     }
     52 
     53     @Override
     54     public AddressVerificationNodeData get(String key) {
     55         JsoMap jso = mCacheData.getObj(key);
     56         if (jso == null) {  // Not cached.
     57             fetchDataIfNotAvailable(key);
     58             jso = mCacheData.getObj(key);
     59         }
     60         if (jso != null && isValidDataKey(key)) {
     61             return createNodeData(jso);
     62         }
     63         return null;
     64     }
     65 
     66     @Override
     67     public AddressVerificationNodeData getDefaultData(String key) {
     68         // root data
     69         if (key.split("/").length == 1) {
     70             JsoMap jso = mBootstrapMap.get(key);
     71             if (jso == null || !isValidDataKey(key)) {
     72                 throw new RuntimeException("key " + key + " does not have bootstrap data");
     73             }
     74             return createNodeData(jso);
     75         }
     76 
     77         key = getCountryKey(key);
     78         JsoMap jso = mBootstrapMap.get(key);
     79         if (jso == null || !isValidDataKey(key)) {
     80             throw new RuntimeException("key " + key + " does not have bootstrap data");
     81         }
     82         return createNodeData(jso);
     83     }
     84 
     85     private String getCountryKey(String hierarchyKey) {
     86         if (hierarchyKey.split("/").length <= 1) {
     87             throw new RuntimeException("Cannot get country key with key '" + hierarchyKey + "'");
     88         }
     89         if (isCountryKey(hierarchyKey)) {
     90             return hierarchyKey;
     91         }
     92 
     93         String[] parts = hierarchyKey.split("/");
     94 
     95         return new StringBuilder().append(parts[0])
     96                 .append("/")
     97                 .append(parts[1])
     98                 .toString();
     99     }
    100 
    101     private boolean isCountryKey(String hierarchyKey) {
    102         Util.checkNotNull(hierarchyKey, "Cannot use null as a key");
    103         return hierarchyKey.split("/").length == 2;
    104     }
    105 
    106 
    107     /**
    108      * Returns the contents of the JSON-format string as a map.
    109      */
    110     protected AddressVerificationNodeData createNodeData(JsoMap jso) {
    111         Map<AddressDataKey, String> map =
    112                 new EnumMap<AddressDataKey, String>(AddressDataKey.class);
    113 
    114         JSONArray arr = jso.getKeys();
    115         for (int i = 0; i < arr.length(); i++) {
    116             try {
    117                 AddressDataKey key = AddressDataKey.get(arr.getString(i));
    118 
    119                 if (key == null) {
    120                     // Not all keys are supported by Android, so we continue if we encounter one
    121                     // that is not used.
    122                     continue;
    123                 }
    124 
    125                 String value = jso.get(key.toString().toLowerCase());
    126                 map.put(key, value);
    127             } catch (JSONException e) {
    128                 // This should not happen - we should not be fetching a key from outside the bounds
    129                 // of the array.
    130             }
    131         }
    132 
    133         return new AddressVerificationNodeData(map);
    134     }
    135 
    136     /**
    137      * We can be initialized with the full set of address information, but validation only uses info
    138      * prefixed with "data" (in particular, no info prefixed with "examples").
    139      */
    140     private boolean isValidDataKey(String key) {
    141         return key.startsWith("data");
    142     }
    143 
    144     /**
    145      * Initializes regionalData structure based on property file.
    146      */
    147     private void buildRegionalData() {
    148         StringBuilder countries = new StringBuilder();
    149 
    150         for (String countryCode : RegionDataConstants.getCountryFormatMap().keySet()) {
    151             countries.append(countryCode + "~");
    152             String json = RegionDataConstants.getCountryFormatMap().get(countryCode);
    153             JsoMap jso = null;
    154             try {
    155                 jso = JsoMap.buildJsoMap(json);
    156             } catch (JSONException e) {
    157                 // Ignore.
    158             }
    159 
    160             AddressData data = new AddressData.Builder().setCountry(countryCode).build();
    161             LookupKey key = new LookupKey.Builder(KeyType.DATA).setAddressData(data).build();
    162             mBootstrapMap.put(key.toString(), jso);
    163         }
    164         countries.setLength(countries.length() - 1);
    165 
    166         // TODO: this is messy. do we have better ways to do it?
    167         /* Creates verification data for key="data". This will be used for the
    168          * root FieldVerifier.
    169          */
    170         String str = "{\"id\":\"data\",\"" +
    171                 AddressDataKey.COUNTRIES.toString().toLowerCase() +
    172                 "\": \"" + countries.toString() + "\"}";
    173         JsoMap jsoData = null;
    174         try {
    175             jsoData = JsoMap.buildJsoMap(str);
    176         } catch (JSONException e) {
    177             // Ignore.
    178         }
    179         mBootstrapMap.put("data", jsoData);
    180     }
    181 
    182     /**
    183      * Fetches data from remote server if it is not cached yet.
    184      *
    185      * @param key The key for data that being requested. Key can be either a data key (starts with
    186      *            "data") or example key (starts with "examples")
    187      */
    188     private void fetchDataIfNotAvailable(String key) {
    189         JsoMap jso = mCacheData.getObj(key);
    190         if (jso == null) {
    191             // If there is bootstrap data for the key, pass the data to fetchDynamicData
    192             JsoMap regionalData = mBootstrapMap.get(key);
    193             NotifyingListener listener = new NotifyingListener(this);
    194             // If the key was invalid, we don't want to attempt to fetch it.
    195             if (LookupKey.hasValidKeyPrefix(key)) {
    196                 LookupKey lookupKey = new LookupKey.Builder(key).build();
    197                 mCacheData.fetchDynamicData(lookupKey, regionalData, listener);
    198                 try {
    199                     listener.waitLoadingEnd();
    200                     // Check to see if there is data for this key now.
    201                     if (mCacheData.getObj(key) == null && isCountryKey(key)) {
    202                         // If not, see if there is data in RegionDataConstants.
    203                         Log.i(TAG, "Server failure: looking up key in region data constants.");
    204                         mCacheData.getFromRegionDataConstants(lookupKey);
    205                     }
    206                 } catch (InterruptedException e) {
    207                     throw new RuntimeException(e);
    208                 }
    209             }
    210         }
    211     }
    212 
    213     public void requestData(LookupKey key, DataLoadListener listener) {
    214         Util.checkNotNull(key, "Null lookup key not allowed");
    215         JsoMap regionalData = mBootstrapMap.get(key.toString());
    216         mCacheData.fetchDynamicData(key, regionalData, listener);
    217     }
    218 
    219     /**
    220      * Fetches all data for the specified country from the remote server.
    221      */
    222     public void prefetchCountry(String country, DataLoadListener listener) {
    223         String key = "data/" + country;
    224         Set<RecursiveLoader> loaders = new HashSet<RecursiveLoader>();
    225         listener.dataLoadingBegin();
    226         mCacheData.fetchDynamicData(
    227                 new LookupKey.Builder(key).build(),
    228                 null,
    229                 new RecursiveLoader(key, loaders, listener));
    230     }
    231 
    232     /**
    233      * A helper class to recursively load all sub keys using fetchDynamicData().
    234      */
    235     private class RecursiveLoader implements DataLoadListener {
    236 
    237         private final String key;
    238 
    239         private final Set<RecursiveLoader> loaders;
    240 
    241         private final DataLoadListener listener;
    242 
    243         public RecursiveLoader(String key, Set<RecursiveLoader> loaders,
    244                 DataLoadListener listener) {
    245             this.key = key;
    246             this.loaders = loaders;
    247             this.listener = listener;
    248 
    249             synchronized (loaders) {
    250                 loaders.add(this);
    251             }
    252         }
    253 
    254         @Override
    255         public void dataLoadingBegin() {
    256         }
    257 
    258         @Override
    259         public void dataLoadingEnd() {
    260             final String subKeys = AddressDataKey.SUB_KEYS.name().toLowerCase();
    261             final String subMores = AddressDataKey.SUB_MORES.name().toLowerCase();
    262 
    263             JsoMap map = mCacheData.getObj(key);
    264 
    265             if (map.containsKey(subMores)) {
    266                 // This key could have sub keys.
    267                 String[] mores = {};
    268                 String[] keys = {};
    269 
    270                 mores = map.get(subMores).split("~");
    271 
    272                 if (map.containsKey(subKeys)) {
    273                     keys = map.get(subKeys).split("~");
    274                 }
    275 
    276                 if (mores.length != keys.length) {  // This should never happen.
    277                     throw new IndexOutOfBoundsException("mores.length != keys.length");
    278                 }
    279 
    280                 for (int i = 0; i < mores.length; i++) {
    281                     if (mores[i].equalsIgnoreCase("true")) {
    282                         // This key should have sub keys.
    283                         String subKey = key + "/" + keys[i];
    284                         mCacheData.fetchDynamicData(
    285                                 new LookupKey.Builder(subKey).build(),
    286                                 null,
    287                                 new RecursiveLoader(subKey, loaders, listener));
    288                     }
    289                 }
    290             }
    291 
    292             synchronized (loaders) {
    293                 loaders.remove(this);
    294                 if (loaders.isEmpty()) {
    295                     listener.dataLoadingEnd();
    296                 }
    297             }
    298         }
    299     }
    300 }
    301