aboutsummaryrefslogtreecommitdiff
path: root/java/src/com/android/i18n/addressinput/ClientData.java
blob: 72be34ff3e7842b472b1749c8763292bf2e4c8e0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
/*
 * Copyright (C) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.i18n.addressinput;

import com.android.i18n.addressinput.LookupKey.KeyType;

import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;

import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Access point for the cached address verification data. The data contained here will mainly be
 * used to build {@link FieldVerifier}'s. This class is implemented as a singleton.
 */
public class ClientData implements DataSource {

    private static final String TAG = "ClientData";

    /**
     * Data to bootstrap the process. The data are all regional (country level)
     * data. Keys are like "data/US/CA"
     */
    private final Map<String, JsoMap> mBootstrapMap = new HashMap<String, JsoMap>();

    private CacheData mCacheData;

    public ClientData(CacheData cacheData) {
        this.mCacheData = cacheData;
        buildRegionalData();
    }

    @Override
    public AddressVerificationNodeData get(String key) {
        JsoMap jso = mCacheData.getObj(key);
        if (jso == null) {  // Not cached.
            fetchDataIfNotAvailable(key);
            jso = mCacheData.getObj(key);
        }
        if (jso != null && isValidDataKey(key)) {
            return createNodeData(jso);
        }
        return null;
    }

    @Override
    public AddressVerificationNodeData getDefaultData(String key) {
        // root data
        if (key.split("/").length == 1) {
            JsoMap jso = mBootstrapMap.get(key);
            if (jso == null || !isValidDataKey(key)) {
                throw new RuntimeException("key " + key + " does not have bootstrap data");
            }
            return createNodeData(jso);
        }

        key = getCountryKey(key);
        JsoMap jso = mBootstrapMap.get(key);
        if (jso == null || !isValidDataKey(key)) {
            throw new RuntimeException("key " + key + " does not have bootstrap data");
        }
        return createNodeData(jso);
    }

    private String getCountryKey(String hierarchyKey) {
        if (hierarchyKey.split("/").length <= 1) {
            throw new RuntimeException("Cannot get country key with key '" + hierarchyKey + "'");
        }
        if (isCountryKey(hierarchyKey)) {
            return hierarchyKey;
        }

        String[] parts = hierarchyKey.split("/");

        return new StringBuilder().append(parts[0])
                .append("/")
                .append(parts[1])
                .toString();
    }

    private boolean isCountryKey(String hierarchyKey) {
        Util.checkNotNull(hierarchyKey, "Cannot use null as a key");
        return hierarchyKey.split("/").length == 2;
    }


    /**
     * Returns the contents of the JSON-format string as a map.
     */
    protected AddressVerificationNodeData createNodeData(JsoMap jso) {
        Map<AddressDataKey, String> map =
                new EnumMap<AddressDataKey, String>(AddressDataKey.class);

        JSONArray arr = jso.getKeys();
        for (int i = 0; i < arr.length(); i++) {
            try {
                AddressDataKey key = AddressDataKey.get(arr.getString(i));

                if (key == null) {
                    // Not all keys are supported by Android, so we continue if we encounter one
                    // that is not used.
                    continue;
                }

                String value = jso.get(key.toString().toLowerCase());
                map.put(key, value);
            } catch (JSONException e) {
                // This should not happen - we should not be fetching a key from outside the bounds
                // of the array.
            }
        }

        return new AddressVerificationNodeData(map);
    }

    /**
     * We can be initialized with the full set of address information, but validation only uses info
     * prefixed with "data" (in particular, no info prefixed with "examples").
     */
    private boolean isValidDataKey(String key) {
        return key.startsWith("data");
    }

    /**
     * Initializes regionalData structure based on property file.
     */
    private void buildRegionalData() {
        StringBuilder countries = new StringBuilder();

        for (String countryCode : RegionDataConstants.getCountryFormatMap().keySet()) {
            countries.append(countryCode + "~");
            String json = RegionDataConstants.getCountryFormatMap().get(countryCode);
            JsoMap jso = null;
            try {
                jso = JsoMap.buildJsoMap(json);
            } catch (JSONException e) {
                // Ignore.
            }

            AddressData data = new AddressData.Builder().setCountry(countryCode).build();
            LookupKey key = new LookupKey.Builder(KeyType.DATA).setAddressData(data).build();
            mBootstrapMap.put(key.toString(), jso);
        }
        countries.setLength(countries.length() - 1);

        // TODO: this is messy. do we have better ways to do it?
        /* Creates verification data for key="data". This will be used for the
         * root FieldVerifier.
         */
        String str = "{\"id\":\"data\",\"" +
                AddressDataKey.COUNTRIES.toString().toLowerCase() +
                "\": \"" + countries.toString() + "\"}";
        JsoMap jsoData = null;
        try {
            jsoData = JsoMap.buildJsoMap(str);
        } catch (JSONException e) {
            // Ignore.
        }
        mBootstrapMap.put("data", jsoData);
    }

    /**
     * Fetches data from remote server if it is not cached yet.
     *
     * @param key The key for data that being requested. Key can be either a data key (starts with
     *            "data") or example key (starts with "examples")
     */
    private void fetchDataIfNotAvailable(String key) {
        JsoMap jso = mCacheData.getObj(key);
        if (jso == null) {
            // If there is bootstrap data for the key, pass the data to fetchDynamicData
            JsoMap regionalData = mBootstrapMap.get(key);
            NotifyingListener listener = new NotifyingListener(this);
            // If the key was invalid, we don't want to attempt to fetch it.
            if (LookupKey.hasValidKeyPrefix(key)) {
                LookupKey lookupKey = new LookupKey.Builder(key).build();
                mCacheData.fetchDynamicData(lookupKey, regionalData, listener);
                try {
                    listener.waitLoadingEnd();
                    // Check to see if there is data for this key now.
                    if (mCacheData.getObj(key) == null && isCountryKey(key)) {
                        // If not, see if there is data in RegionDataConstants.
                        Log.i(TAG, "Server failure: looking up key in region data constants.");
                        mCacheData.getFromRegionDataConstants(lookupKey);
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    public void requestData(LookupKey key, DataLoadListener listener) {
        Util.checkNotNull(key, "Null lookup key not allowed");
        JsoMap regionalData = mBootstrapMap.get(key.toString());
        mCacheData.fetchDynamicData(key, regionalData, listener);
    }

    /**
     * Fetches all data for the specified country from the remote server.
     */
    public void prefetchCountry(String country, DataLoadListener listener) {
        String key = "data/" + country;
        Set<RecursiveLoader> loaders = new HashSet<RecursiveLoader>();
        listener.dataLoadingBegin();
        mCacheData.fetchDynamicData(
                new LookupKey.Builder(key).build(),
                null,
                new RecursiveLoader(key, loaders, listener));
    }

    /**
     * A helper class to recursively load all sub keys using fetchDynamicData().
     */
    private class RecursiveLoader implements DataLoadListener {

        private final String key;

        private final Set<RecursiveLoader> loaders;

        private final DataLoadListener listener;

        public RecursiveLoader(String key, Set<RecursiveLoader> loaders,
                DataLoadListener listener) {
            this.key = key;
            this.loaders = loaders;
            this.listener = listener;

            synchronized (loaders) {
                loaders.add(this);
            }
        }

        @Override
        public void dataLoadingBegin() {
        }

        @Override
        public void dataLoadingEnd() {
            final String subKeys = AddressDataKey.SUB_KEYS.name().toLowerCase();
            final String subMores = AddressDataKey.SUB_MORES.name().toLowerCase();

            JsoMap map = mCacheData.getObj(key);

            if (map.containsKey(subMores)) {
                // This key could have sub keys.
                String[] mores = {};
                String[] keys = {};

                mores = map.get(subMores).split("~");

                if (map.containsKey(subKeys)) {
                    keys = map.get(subKeys).split("~");
                }

                if (mores.length != keys.length) {  // This should never happen.
                    throw new IndexOutOfBoundsException("mores.length != keys.length");
                }

                for (int i = 0; i < mores.length; i++) {
                    if (mores[i].equalsIgnoreCase("true")) {
                        // This key should have sub keys.
                        String subKey = key + "/" + keys[i];
                        mCacheData.fetchDynamicData(
                                new LookupKey.Builder(subKey).build(),
                                null,
                                new RecursiveLoader(subKey, loaders, listener));
                    }
                }
            }

            synchronized (loaders) {
                loaders.remove(this);
                if (loaders.isEmpty()) {
                    listener.dataLoadingEnd();
                }
            }
        }
    }
}