Upstream version 8.36.169.0
[platform/framework/web/crosswalk.git] / src / third_party / libaddressinput / src / java / src / com / android / i18n / addressinput / FormController.java
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 import com.android.i18n.addressinput.LookupKey.ScriptType;
21
22 import java.util.ArrayList;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Queue;
26
27 /**
28  * Responsible for looking up data for address fields. This fetches possible
29  * values for the next level down in the address hierarchy, if these are known.
30  */
31 class FormController {
32     // For address hierarchy in lookup key.
33     private static final String SLASH_DELIM = "/";
34     // For joined values.
35     private static final String TILDE_DELIM = "~";
36     // For language code info in lookup key (E.g., data/CA--fr).
37     private static final String DASH_DELIM = "--";
38     private static final LookupKey ROOT_KEY = FormController.getDataKeyForRoot();
39     private static final String DEFAULT_REGION_CODE = "ZZ";
40     private static final AddressField[] ADDRESS_HIERARCHY = {
41             AddressField.COUNTRY,
42             AddressField.ADMIN_AREA,
43             AddressField.LOCALITY,
44             AddressField.DEPENDENT_LOCALITY
45     };
46
47     // Current user language.
48     private String mLanguageCode;
49     private ClientData mIntegratedData;
50     private String mCurrentCountry;
51
52     /**
53      * Constructor that populates this with data. languageCode should be a BCP language code (such
54      * as "en" or "zh-Hant") and currentCountry should be an ISO 2-letter region code (such as "GB"
55      * or "US").
56      */
57     FormController(ClientData integratedData, String languageCode, String currentCountry) {
58         Util.checkNotNull(integratedData, "null data not allowed");
59         mLanguageCode = languageCode;
60         this.mCurrentCountry = currentCountry;
61
62         AddressData address = new AddressData.Builder().setCountry(DEFAULT_REGION_CODE).build();
63         LookupKey defaultCountryKey = getDataKeyFor(address);
64
65         AddressVerificationNodeData defaultCountryData =
66             integratedData.getDefaultData(defaultCountryKey.toString());
67         Util.checkNotNull(defaultCountryData,
68                 "require data for default country key: " + defaultCountryKey);
69         this.mIntegratedData = integratedData;
70     }
71
72     void setLanguageCode(String languageCode) {
73         mLanguageCode = languageCode;
74     }
75
76     void setCurrentCountry(String currentCountry) {
77         mCurrentCountry = currentCountry;
78     }
79
80     private ScriptType getScriptType() {
81         if (mLanguageCode != null && Util.isExplicitLatinScript(mLanguageCode)) {
82             return ScriptType.LATIN;
83         }
84         return ScriptType.LOCAL;
85     }
86
87     private static LookupKey getDataKeyForRoot() {
88         AddressData address = new AddressData.Builder().build();
89         return new LookupKey.Builder(KeyType.DATA).setAddressData(address).build();
90     }
91
92     LookupKey getDataKeyFor(AddressData address) {
93         return new LookupKey.Builder(KeyType.DATA).setAddressData(address).build();
94     }
95
96     /**
97      * Requests data for the input address. This method chains multiple requests together. For
98      * example, an address for Mt View, California needs data from "data/US", "data/US/CA", and
99      * "data/US/CA/Mt View" to support it. This method will request them one by one (from top level
100      * key down to the most granular) and evokes {@link DataLoadListener#dataLoadingEnd} method when
101      * all data is collected. If the address is invalid, it will request the first valid child key
102      * instead. For example, a request for "data/US/Foo" will end up requesting data for "data/US",
103      * "data/US/AL".
104      *
105      * @param address  the current address.
106      * @param listener triggered when requested data for the address is returned.
107      */
108     void requestDataForAddress(AddressData address, DataLoadListener listener) {
109         Util.checkNotNull(address.getPostalCountry(), "null country not allowed");
110
111         // Gets the key for deepest available node.
112         Queue<String> subkeys = new LinkedList<String>();
113
114         for (AddressField field : ADDRESS_HIERARCHY) {
115             String value = address.getFieldValue(field);
116             if (value == null) {
117                 break;
118             }
119             subkeys.add(value);
120         }
121         if (subkeys.size() == 0) {
122             throw new RuntimeException("Need at least country level info");
123         }
124
125         if (listener != null) {
126             listener.dataLoadingBegin();
127         }
128         requestDataRecursively(ROOT_KEY, subkeys, listener);
129     }
130
131     private void requestDataRecursively(final LookupKey key,
132             final Queue<String> subkeys, final DataLoadListener listener) {
133         Util.checkNotNull(key, "Null key not allowed");
134         Util.checkNotNull(subkeys, "Null subkeys not allowed");
135
136         mIntegratedData.requestData(key, new DataLoadListener() {
137             @Override
138             public void dataLoadingBegin() {
139             }
140
141             @Override
142             public void dataLoadingEnd() {
143                 List<RegionData> subregions = getRegionData(key);
144                 if (subregions.isEmpty()) {
145                     if (listener != null) {
146                         listener.dataLoadingEnd();
147                     }
148                     // TODO: Should update the selectors here.
149                     return;
150                 } else if (subkeys.size() > 0) {
151                     String subkey = subkeys.remove();
152                     for (RegionData subregion : subregions) {
153                         if (subregion.isValidName(subkey)) {
154                             LookupKey nextKey = buildDataLookupKey(key, subregion.getKey());
155                             requestDataRecursively(nextKey, subkeys, listener);
156                             return;
157                         }
158                     }
159                 }
160
161                 // Current value in the field is not valid, use the first valid subkey
162                 // to request more data instead.
163                 String firstSubkey = subregions.get(0).getKey();
164                 LookupKey nextKey = buildDataLookupKey(key, firstSubkey);
165                 Queue<String> emptyList = new LinkedList<String>();
166                 requestDataRecursively(nextKey, emptyList, listener);
167             }
168         });
169     }
170
171     private LookupKey buildDataLookupKey(LookupKey lookupKey, String subKey) {
172         String[] subKeys = lookupKey.toString().split(SLASH_DELIM);
173         String languageCodeSubTag =
174                 (mLanguageCode == null) ? null : Util.getLanguageSubtag(mLanguageCode);
175         String key = lookupKey.toString() + SLASH_DELIM + subKey;
176
177         // Country level key
178         if (subKeys.length == 1 &&
179                 languageCodeSubTag != null && !isDefaultLanguage(languageCodeSubTag)) {
180             key += DASH_DELIM + languageCodeSubTag.toString();
181         }
182         return new LookupKey.Builder(key).build();
183     }
184
185     /**
186      * Compares the language subtags of input {@code languageCode} and default language code. For
187      * example, "zh-Hant" and "zh" are viewed as identical.
188      */
189     boolean isDefaultLanguage(String languageCode) {
190         if (languageCode == null) {
191             return true;
192         }
193         AddressData addr = new AddressData.Builder().setCountry(mCurrentCountry).build();
194         LookupKey lookupKey = getDataKeyFor(addr);
195         AddressVerificationNodeData data =
196                 mIntegratedData.getDefaultData(lookupKey.toString());
197         String defaultLanguage = data.get(AddressDataKey.LANG);
198
199         // Current language is not the default language for the country.
200         if (Util.trimToNull(defaultLanguage) != null &&
201             !Util.getLanguageSubtag(languageCode).equals(Util.getLanguageSubtag(languageCode))) {
202             return false;
203         }
204         return true;
205     }
206
207     /**
208      * Gets a list of {@link RegionData} for sub-regions for a given key. For example, sub regions
209      * for "data/US" are AL/Alabama, AK/Alaska, etc.
210      *
211      * <p> TODO: It seems more straight forward to return a list of pairs instead of RegionData.
212      * Actually, we can remove RegionData since it does not contain anything more than key/value
213      * pairs now.
214      *
215      * @return A list of sub-regions, each sub-region represented by a {@link RegionData}.
216      */
217     List<RegionData> getRegionData(LookupKey key) {
218         if (key.getKeyType() == KeyType.EXAMPLES) {
219             throw new RuntimeException("example key not allowed for getting region data");
220         }
221         Util.checkNotNull(key, "null regionKey not allowed");
222         LookupKey normalizedKey = normalizeLookupKey(key);
223         List<RegionData> results = new ArrayList<RegionData>();
224
225         // Root key.
226         if (normalizedKey.equals(ROOT_KEY)) {
227             AddressVerificationNodeData data =
228                     mIntegratedData.getDefaultData(normalizedKey.toString());
229             String[] countries = splitData(data.get(AddressDataKey.COUNTRIES));
230             for (int i = 0; i < countries.length; i++) {
231                 RegionData rd = new RegionData.Builder()
232                         .setKey(countries[i])
233                         .setName(countries[i])
234                         .build();
235                 results.add(rd);
236             }
237             return results;
238         }
239
240         AddressVerificationNodeData data =
241                 mIntegratedData.get(normalizedKey.toString());
242         if (data != null) {
243             String[] keys = splitData(data.get(AddressDataKey.SUB_KEYS));
244             String[] names = (getScriptType() == ScriptType.LOCAL)
245                     ? splitData(data.get(AddressDataKey.SUB_NAMES))
246                     : splitData(data.get(AddressDataKey.SUB_LNAMES));
247
248             for (int i = 0; i < keys.length; i++) {
249                 RegionData rd =
250                         new RegionData.Builder()
251                                 .setKey(keys[i])
252                                 .setName((i < names.length) ? names[i] : keys[i])
253                                 .build();
254                 results.add(rd);
255             }
256         }
257         return results;
258     }
259
260     /**
261      * Split a '~' delimited string into an array of strings. This method is null tolerant and
262      * considers an empty string to contain no elements.
263      *
264      * @param raw The data to split
265      * @return an array of strings
266      */
267     private String[] splitData(String raw) {
268         if (raw == null || raw.length() == 0) {
269             return new String[]{};
270         }
271         return raw.split(TILDE_DELIM);
272     }
273
274     private String getSubKey(LookupKey parentKey, String name) {
275         for (RegionData subRegion : getRegionData(parentKey)) {
276             if (subRegion.isValidName(name)) {
277                 return subRegion.getKey();
278             }
279         }
280         return null;
281     }
282
283     /**
284      * Normalizes {@code key} by replacing field values with sub-keys. For example, California is
285      * replaced with CA. The normalization goes from top (country) to bottom (dependent locality)
286      * and if any field value is empty, unknown, or invalid, it will stop and return whatever it
287      * gets. For example, a key "data/US/California/foobar/kar" will be normalized into
288      * "data/US/CA/foobar/kar" since "foobar" is unknown. This method supports only key of
289      * {@link KeyType#DATA} type.
290      *
291      * @return normalized {@link LookupKey}.
292      */
293     private LookupKey normalizeLookupKey(LookupKey key) {
294         Util.checkNotNull(key);
295         if (key.getKeyType() != KeyType.DATA) {
296             throw new RuntimeException("Only DATA keyType is supported");
297         }
298
299         String subStr[] = key.toString().split(SLASH_DELIM);
300
301         // Root key does not need to be normalized.
302         if (subStr.length < 2) {
303             return key;
304         }
305
306         StringBuilder sb = new StringBuilder(subStr[0]);
307         for (int i = 1; i < subStr.length; ++i) {
308             // Strips the language code if contained.
309             String languageCode = null;
310             if (i == 1 && subStr[i].contains(DASH_DELIM)) {
311                 String[] s = subStr[i].split(DASH_DELIM);
312                 subStr[i] = s[0];
313                 languageCode = s[1];
314             }
315
316             String normalizedSubKey = getSubKey(new LookupKey.Builder(sb.toString()).build(),
317                     subStr[i]);
318
319             // Can't find normalized sub-key; assembles the lookup key with the
320             // remaining sub-keys and returns it.
321             if (normalizedSubKey == null) {
322                 for (; i < subStr.length; ++i) {
323                     sb.append(SLASH_DELIM).append(subStr[i]);
324                 }
325                 break;
326             }
327             sb.append(SLASH_DELIM).append(normalizedSubKey);
328             if (languageCode != null) {
329                 sb.append(DASH_DELIM).append(languageCode);
330             }
331         }
332         return new LookupKey.Builder(sb.toString()).build();
333     }
334 }