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;
20 import com.android.i18n.addressinput.LookupKey.ScriptType;
22 import java.util.ArrayList;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Queue;
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.
31 class FormController {
32 // For address hierarchy in lookup key.
33 private static final String SLASH_DELIM = "/";
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 = {
42 AddressField.ADMIN_AREA,
43 AddressField.LOCALITY,
44 AddressField.DEPENDENT_LOCALITY
47 // Current user language.
48 private String mLanguageCode;
49 private ClientData mIntegratedData;
50 private String mCurrentCountry;
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"
57 FormController(ClientData integratedData, String languageCode, String currentCountry) {
58 Util.checkNotNull(integratedData, "null data not allowed");
59 mLanguageCode = languageCode;
60 this.mCurrentCountry = currentCountry;
62 AddressData address = new AddressData.Builder().setCountry(DEFAULT_REGION_CODE).build();
63 LookupKey defaultCountryKey = getDataKeyFor(address);
65 AddressVerificationNodeData defaultCountryData =
66 integratedData.getDefaultData(defaultCountryKey.toString());
67 Util.checkNotNull(defaultCountryData,
68 "require data for default country key: " + defaultCountryKey);
69 this.mIntegratedData = integratedData;
72 void setLanguageCode(String languageCode) {
73 mLanguageCode = languageCode;
76 void setCurrentCountry(String currentCountry) {
77 mCurrentCountry = currentCountry;
80 private ScriptType getScriptType() {
81 if (mLanguageCode != null && Util.isExplicitLatinScript(mLanguageCode)) {
82 return ScriptType.LATIN;
84 return ScriptType.LOCAL;
87 private static LookupKey getDataKeyForRoot() {
88 AddressData address = new AddressData.Builder().build();
89 return new LookupKey.Builder(KeyType.DATA).setAddressData(address).build();
92 LookupKey getDataKeyFor(AddressData address) {
93 return new LookupKey.Builder(KeyType.DATA).setAddressData(address).build();
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",
105 * @param address the current address.
106 * @param listener triggered when requested data for the address is returned.
108 void requestDataForAddress(AddressData address, DataLoadListener listener) {
109 Util.checkNotNull(address.getPostalCountry(), "null country not allowed");
111 // Gets the key for deepest available node.
112 Queue<String> subkeys = new LinkedList<String>();
114 for (AddressField field : ADDRESS_HIERARCHY) {
115 String value = address.getFieldValue(field);
121 if (subkeys.size() == 0) {
122 throw new RuntimeException("Need at least country level info");
125 if (listener != null) {
126 listener.dataLoadingBegin();
128 requestDataRecursively(ROOT_KEY, subkeys, listener);
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");
136 mIntegratedData.requestData(key, new DataLoadListener() {
138 public void dataLoadingBegin() {
142 public void dataLoadingEnd() {
143 List<RegionData> subregions = getRegionData(key);
144 if (subregions.isEmpty()) {
145 if (listener != null) {
146 listener.dataLoadingEnd();
148 // TODO: Should update the selectors here.
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);
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);
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;
178 if (subKeys.length == 1 &&
179 languageCodeSubTag != null && !isDefaultLanguage(languageCodeSubTag)) {
180 key += DASH_DELIM + languageCodeSubTag.toString();
182 return new LookupKey.Builder(key).build();
186 * Compares the language subtags of input {@code languageCode} and default language code. For
187 * example, "zh-Hant" and "zh" are viewed as identical.
189 boolean isDefaultLanguage(String languageCode) {
190 if (languageCode == null) {
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);
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))) {
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.
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
215 * @return A list of sub-regions, each sub-region represented by a {@link RegionData}.
217 List<RegionData> getRegionData(LookupKey key) {
218 if (key.getKeyType() == KeyType.EXAMPLES) {
219 throw new RuntimeException("example key not allowed for getting region data");
221 Util.checkNotNull(key, "null regionKey not allowed");
222 LookupKey normalizedKey = normalizeLookupKey(key);
223 List<RegionData> results = new ArrayList<RegionData>();
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])
240 AddressVerificationNodeData data =
241 mIntegratedData.get(normalizedKey.toString());
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));
248 for (int i = 0; i < keys.length; i++) {
250 new RegionData.Builder()
252 .setName((i < names.length) ? names[i] : keys[i])
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.
264 * @param raw The data to split
265 * @return an array of strings
267 private String[] splitData(String raw) {
268 if (raw == null || raw.length() == 0) {
269 return new String[]{};
271 return raw.split(TILDE_DELIM);
274 private String getSubKey(LookupKey parentKey, String name) {
275 for (RegionData subRegion : getRegionData(parentKey)) {
276 if (subRegion.isValidName(name)) {
277 return subRegion.getKey();
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.
291 * @return normalized {@link LookupKey}.
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");
299 String subStr[] = key.toString().split(SLASH_DELIM);
301 // Root key does not need to be normalized.
302 if (subStr.length < 2) {
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);
316 String normalizedSubKey = getSubKey(new LookupKey.Builder(sb.toString()).build(),
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]);
327 sb.append(SLASH_DELIM).append(normalizedSubKey);
328 if (languageCode != null) {
329 sb.append(DASH_DELIM).append(languageCode);
332 return new LookupKey.Builder(sb.toString()).build();