2 * Copyright (C) 2010 Google Inc.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.i18n.addressinput;
19 import com.android.i18n.addressinput.LookupKey.KeyType;
21 import android.util.Log;
23 import org.json.JSONArray;
24 import org.json.JSONException;
26 import java.util.EnumMap;
27 import java.util.HashMap;
28 import java.util.HashSet;
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.
36 public class ClientData implements DataSource {
38 private static final String TAG = "ClientData";
41 * Data to bootstrap the process. The data are all regional (country level)
42 * data. Keys are like "data/US/CA"
44 private final Map<String, JsoMap> bootstrapMap = new HashMap<String, JsoMap>();
46 private CacheData cacheData;
48 public ClientData(CacheData cacheData) {
49 this.cacheData = cacheData;
54 public AddressVerificationNodeData get(String key) {
55 JsoMap jso = cacheData.getObj(key);
56 if (jso == null) { // Not cached.
57 fetchDataIfNotAvailable(key);
58 jso = cacheData.getObj(key);
60 if (jso != null && isValidDataKey(key)) {
61 return createNodeData(jso);
67 public AddressVerificationNodeData getDefaultData(String key) {
69 if (key.split("/").length == 1) {
70 JsoMap jso = bootstrapMap.get(key);
71 if (jso == null || !isValidDataKey(key)) {
72 throw new RuntimeException("key " + key + " does not have bootstrap data");
74 return createNodeData(jso);
77 key = getCountryKey(key);
78 JsoMap jso = bootstrapMap.get(key);
79 if (jso == null || !isValidDataKey(key)) {
80 throw new RuntimeException("key " + key + " does not have bootstrap data");
82 return createNodeData(jso);
85 private String getCountryKey(String hierarchyKey) {
86 if (hierarchyKey.split("/").length <= 1) {
87 throw new RuntimeException("Cannot get country key with key '" + hierarchyKey + "'");
89 if (isCountryKey(hierarchyKey)) {
93 String[] parts = hierarchyKey.split("/");
95 return new StringBuilder().append(parts[0])
101 private boolean isCountryKey(String hierarchyKey) {
102 Util.checkNotNull(hierarchyKey, "Cannot use null as a key");
103 return hierarchyKey.split("/").length == 2;
108 * Returns the contents of the JSON-format string as a map.
110 protected AddressVerificationNodeData createNodeData(JsoMap jso) {
111 Map<AddressDataKey, String> map =
112 new EnumMap<AddressDataKey, String>(AddressDataKey.class);
114 JSONArray arr = jso.getKeys();
115 for (int i = 0; i < arr.length(); i++) {
117 AddressDataKey key = AddressDataKey.get(arr.getString(i));
120 // Not all keys are supported by Android, so we continue if we encounter one
125 String value = jso.get(key.toString().toLowerCase());
127 } catch (JSONException e) {
128 // This should not happen - we should not be fetching a key from outside the bounds
133 return new AddressVerificationNodeData(map);
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").
140 private boolean isValidDataKey(String key) {
141 return key.startsWith("data");
145 * Initializes regionalData structure based on property file.
147 private void buildRegionalData() {
148 StringBuilder countries = new StringBuilder();
150 for (String countryCode : RegionDataConstants.getCountryFormatMap().keySet()) {
151 countries.append(countryCode + "~");
152 String json = RegionDataConstants.getCountryFormatMap().get(countryCode);
155 jso = JsoMap.buildJsoMap(json);
156 } catch (JSONException e) {
160 AddressData data = new AddressData.Builder().setCountry(countryCode).build();
161 LookupKey key = new LookupKey.Builder(KeyType.DATA).setAddressData(data).build();
162 bootstrapMap.put(key.toString(), jso);
164 countries.setLength(countries.length() - 1);
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.
170 String str = "{\"id\":\"data\",\"" +
171 AddressDataKey.COUNTRIES.toString().toLowerCase() +
172 "\": \"" + countries.toString() + "\"}";
173 JsoMap jsoData = null;
175 jsoData = JsoMap.buildJsoMap(str);
176 } catch (JSONException e) {
179 bootstrapMap.put("data", jsoData);
183 * Fetches data from remote server if it is not cached yet.
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")
188 private void fetchDataIfNotAvailable(String key) {
189 JsoMap jso = cacheData.getObj(key);
191 // If there is bootstrap data for the key, pass the data to fetchDynamicData
192 JsoMap regionalData = bootstrapMap.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 cacheData.fetchDynamicData(lookupKey, regionalData, listener);
199 listener.waitLoadingEnd();
200 // Check to see if there is data for this key now.
201 if (cacheData.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 cacheData.getFromRegionDataConstants(lookupKey);
206 } catch (InterruptedException e) {
207 throw new RuntimeException(e);
213 public void requestData(LookupKey key, DataLoadListener listener) {
214 Util.checkNotNull(key, "Null lookup key not allowed");
215 JsoMap regionalData = bootstrapMap.get(key.toString());
216 cacheData.fetchDynamicData(key, regionalData, listener);
220 * Fetches all data for the specified country from the remote server.
222 public void prefetchCountry(String country, DataLoadListener listener) {
223 String key = "data/" + country;
224 Set<RecursiveLoader> loaders = new HashSet<RecursiveLoader>();
225 listener.dataLoadingBegin();
226 cacheData.fetchDynamicData(
227 new LookupKey.Builder(key).build(),
229 new RecursiveLoader(key, loaders, listener));
233 * A helper class to recursively load all sub keys using fetchDynamicData().
235 private class RecursiveLoader implements DataLoadListener {
237 private final String key;
239 private final Set<RecursiveLoader> loaders;
241 private final DataLoadListener listener;
243 public RecursiveLoader(String key, Set<RecursiveLoader> loaders,
244 DataLoadListener listener) {
246 this.loaders = loaders;
247 this.listener = listener;
249 synchronized (loaders) {
255 public void dataLoadingBegin() {
259 public void dataLoadingEnd() {
260 final String subKeys = AddressDataKey.SUB_KEYS.name().toLowerCase();
261 final String subMores = AddressDataKey.SUB_MORES.name().toLowerCase();
263 JsoMap map = cacheData.getObj(key);
265 if (map.containsKey(subMores)) {
266 // This key could have sub keys.
267 String[] mores = map.get(subMores).split("~");
270 if (map.containsKey(subKeys)) {
271 keys = map.get(subKeys).split("~");
274 if (mores.length != keys.length) { // This should never happen.
275 throw new IndexOutOfBoundsException("mores.length != keys.length");
278 for (int i = 0; i < mores.length; i++) {
279 if (mores[i].equalsIgnoreCase("true")) {
280 // This key should have sub keys.
281 String subKey = key + "/" + keys[i];
282 cacheData.fetchDynamicData(
283 new LookupKey.Builder(subKey).build(),
285 new RecursiveLoader(subKey, loaders, listener));
290 synchronized (loaders) {
291 loaders.remove(this);
292 if (loaders.isEmpty()) {
293 listener.dataLoadingEnd();