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