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
17package com.android.i18n.addressinput;
18
19import com.android.i18n.addressinput.LookupKey.KeyType;
20
21import android.util.Log;
22
23import org.json.JSONArray;
24import org.json.JSONException;
25
26import java.util.EnumMap;
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.Map;
30import 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 */
36public 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