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 java.util.EnumMap; 20 import java.util.Map; 21 22 /** 23 * A builder for creating keys that are used to lookup data in the local cache and fetch data from 24 * the server. There are two key types: {@code KeyType#DATA} or {@code KeyType#EXAMPLES}. 25 * 26 * <p> The {@code KeyType#DATA} key is built based on a universal Address hierarchy, which is:<br> 27 * 28 * {@code AddressField#Country} -> {@code AddressField#ADMIN_AREA} -> {@code AddressField#Locality} 29 * -> {@code AddressField#DEPENDENT_LOCALITY} </p> 30 * 31 * <p> The {@code KeyType#EXAMPLES} key is built with the following format:<br> 32 * 33 * {@code AddressField#Country} -> {@code ScriptType} -> language. </p> 34 */ 35 final class LookupKey { 36 37 /** 38 * Key types. Address Widget organizes address info based on key types. For example, if you want 39 * to know how to verify or format an US address, you need to use {@link KeyType#DATA} to get 40 * that info; if you want to get an example address, you use {@link KeyType#EXAMPLES} instead. 41 */ 42 enum KeyType { 43 44 /** 45 * Key type for getting address data. 46 */ 47 DATA, 48 /** 49 * Key type for getting examples. 50 */ 51 EXAMPLES 52 } 53 54 /** 55 * Script types. This is used for countries that do not use Latin script, but accept it for 56 * transcribing their addresses. For example, you can write a Japanese address in Latin script 57 * instead of Japanese: 58 * 59 * <p> 7-2, Marunouchi 2-Chome, Chiyoda-ku, Tokyo 100-8799 </p> 60 * 61 * Notice that {@link ScriptType} is based on country/region, not language. 62 */ 63 enum ScriptType { 64 65 /** 66 * The script that uses Roman characters like ABC (as opposed to scripts like Cyrillic or 67 * Arabic). 68 */ 69 LATIN, 70 71 /** 72 * Local scripts. For Japan, it's Japanese (including Hiragana, Katagana, and Kanji); For 73 * Saudi Arabia, it's Arabic. Notice that for US, the local script is actually Latin script 74 * (The same goes for other countries that use Latin script). For these countries, we do not 75 * provide two set of data (Latin and local) since they use only Latin script. You have to 76 * specify the {@link ScriptType} as local instead Latin. 77 */ 78 LOCAL 79 } 80 81 /** 82 * The universal address hierarchy. Notice that sub-administrative area is neglected here since 83 * it is not required to fill out address form. 84 */ 85 private static final AddressField[] HIERARCHY = { 86 AddressField.COUNTRY, 87 AddressField.ADMIN_AREA, 88 AddressField.LOCALITY, 89 AddressField.DEPENDENT_LOCALITY}; 90 91 private static final String SLASH_DELIM = "/"; 92 93 private static final String DASH_DELIM = "--"; 94 95 private static final String DEFAULT_LANGUAGE = "_default"; 96 97 private final KeyType mKeyType; 98 99 private final ScriptType mScriptType; 100 101 // Values for hierarchy address fields. 102 private final Map<AddressField, String> mNodes; 103 104 private final String mKeyString; 105 106 private final String mLanguageCode; 107 108 private LookupKey(Builder builder) { 109 this.mKeyType = builder.keyType; 110 this.mScriptType = builder.script; 111 this.mNodes = builder.nodes; 112 this.mLanguageCode = builder.languageCode; 113 this.mKeyString = getKeyString(); 114 } 115 116 /** 117 * Gets lookup key for the input address field. This method does not allow key with key type of 118 * {@link KeyType#EXAMPLES}. 119 * 120 * @param field a field in the address hierarchy. 121 * @return key of the specified address field. If address field is not in the hierarchy, or is 122 * more granular than the current key has, returns null. For example, if your current 123 * key is "data/US" (down to country level), and you want to get the key for Locality 124 * (more granular than country), it will return null. 125 */ 126 LookupKey getKeyForUpperLevelField(AddressField field) { 127 if (mKeyType != KeyType.DATA) { 128 // We only support getting the parent key for the data key type. 129 throw new RuntimeException("Only support getting parent keys for the data key type."); 130 } 131 Builder newKeyBuilder = new Builder(this); 132 133 boolean removeNode = false; 134 boolean fieldInHierarchy = false; 135 for (AddressField hierarchyField : HIERARCHY) { 136 if (removeNode) { 137 if (newKeyBuilder.nodes.containsKey(hierarchyField)) { 138 newKeyBuilder.nodes.remove(hierarchyField); 139 } 140 } 141 if (hierarchyField == field) { 142 if (!newKeyBuilder.nodes.containsKey(hierarchyField)) { 143 return null; 144 } 145 removeNode = true; 146 fieldInHierarchy = true; 147 } 148 } 149 150 if (!fieldInHierarchy) { 151 return null; 152 } 153 154 newKeyBuilder.languageCode = mLanguageCode; 155 newKeyBuilder.script = mScriptType; 156 157 return newKeyBuilder.build(); 158 } 159 160 /** 161 * Returns the string value of a field in a key for a particular 162 * AddressField. For example, for the key "data/US/CA" and the address 163 * field AddressField.COUNTRY, "US" would be returned. Returns an empty 164 * string if the key does not have this field in it. 165 */ 166 String getValueForUpperLevelField(AddressField field) { 167 // First, get the key for this field. 168 LookupKey key = getKeyForUpperLevelField(field); 169 // Now we know the last value in the string is the value for this field. 170 if (key != null) { 171 String keyString = key.toString(); 172 int lastSlashPosition = keyString.lastIndexOf(SLASH_DELIM); 173 if (lastSlashPosition > 0 && lastSlashPosition != keyString.length()) { 174 return keyString.substring(lastSlashPosition + 1); 175 } 176 } 177 return ""; 178 } 179 180 /** 181 * Gets parent key for data key. For example, parent key for "data/US/CA" is "data/US". This 182 * method does not allow key with key type of {@link KeyType#EXAMPLES}. 183 */ 184 LookupKey getParentKey() { 185 if (mKeyType != KeyType.DATA) { 186 throw new RuntimeException("Only support getting parent keys for the data key type."); 187 } 188 // Root key's parent should be null. 189 if (!mNodes.containsKey(AddressField.COUNTRY)) { 190 return null; 191 } 192 193 Builder parentKeyBuilder = new Builder(this); 194 AddressField mostGranularField = AddressField.COUNTRY; 195 196 for (AddressField hierarchyField : HIERARCHY) { 197 if (!mNodes.containsKey(hierarchyField)) { 198 break; 199 } 200 mostGranularField = hierarchyField; 201 } 202 parentKeyBuilder.nodes.remove(mostGranularField); 203 return parentKeyBuilder.build(); 204 } 205 206 KeyType getKeyType() { 207 return mKeyType; 208 } 209 210 /** 211 * Gets a key in string format. E.g., "data/US/CA". 212 */ 213 private String getKeyString() { 214 StringBuilder keyBuilder = new StringBuilder(mKeyType.name().toLowerCase()); 215 216 if (mKeyType == KeyType.DATA) { 217 for (AddressField field : HIERARCHY) { 218 if (!mNodes.containsKey(field)) { 219 break; 220 } 221 if (field == AddressField.COUNTRY && mLanguageCode != null) { 222 keyBuilder.append(SLASH_DELIM) 223 .append(mNodes.get(field)).append(DASH_DELIM) 224 .append(mLanguageCode); 225 } else { 226 keyBuilder.append(SLASH_DELIM).append(mNodes.get(field)); 227 } 228 } 229 } else { 230 if (mNodes.containsKey(AddressField.COUNTRY)) { 231 // Example key. E.g., "examples/TW/local/_default". 232 keyBuilder.append(SLASH_DELIM).append(mNodes.get(AddressField.COUNTRY)) 233 .append(SLASH_DELIM).append(mScriptType.name().toLowerCase()) 234 .append(SLASH_DELIM).append(DEFAULT_LANGUAGE); 235 } 236 } 237 238 return keyBuilder.toString(); 239 } 240 241 /** 242 * Gets a lookup key as a plain text string., e.g., "data/US/CA". 243 */ 244 @Override 245 public String toString() { 246 return mKeyString; 247 } 248 249 @Override 250 public boolean equals(Object obj) { 251 if (this == obj) { 252 return true; 253 } 254 if ((obj == null) || (obj.getClass() != this.getClass())) { 255 return false; 256 } 257 258 return ((LookupKey) obj).toString().equals(mKeyString); 259 } 260 261 @Override 262 public int hashCode() { 263 return mKeyString.hashCode(); 264 } 265 266 static boolean hasValidKeyPrefix(String key) { 267 for (KeyType type : KeyType.values()) { 268 if (key.startsWith(type.name().toLowerCase())) { 269 return true; 270 } 271 } 272 return false; 273 } 274 275 /** 276 * Builds lookup keys. 277 */ 278 static class Builder { 279 280 private KeyType keyType; 281 282 // Default to LOCAL script. 283 284 private ScriptType script = ScriptType.LOCAL; 285 286 private Map<AddressField, String> nodes = new EnumMap<AddressField, String>( 287 AddressField.class); 288 289 private String languageCode; 290 291 /** 292 * Creates a new builder for the specified key type. keyType cannot be null. 293 */ 294 Builder(KeyType keyType) { 295 this.keyType = keyType; 296 } 297 298 /** 299 * Creates a new builder for the specified key. oldKey cannot be null. 300 */ 301 Builder(LookupKey oldKey) { 302 this.keyType = oldKey.mKeyType; 303 this.script = oldKey.mScriptType; 304 this.languageCode = oldKey.mLanguageCode; 305 for (AddressField field : HIERARCHY) { 306 if (!oldKey.mNodes.containsKey(field)) { 307 break; 308 } 309 this.nodes.put(field, oldKey.mNodes.get(field)); 310 } 311 } 312 313 /** 314 * Builds the {@link LookupKey} with the input key string. Input string has to represent 315 * either a {@link KeyType#DATA} key or a {@link KeyType#EXAMPLES} key. Also, key hierarchy 316 * deeper than {@link AddressField#DEPENDENT_LOCALITY} is not allowed. Notice that if any 317 * node in the hierarchy is empty, all the descendant nodes' values will be neglected. For 318 * example, input string "data/US//Mt View" will become "data/US". 319 * 320 * @param keyString e.g., "data/US/CA" 321 */ 322 Builder(String keyString) { 323 String[] parts = keyString.split(SLASH_DELIM); 324 // Check some pre-conditions. 325 if (!parts[0].equals(KeyType.DATA.name().toLowerCase()) && 326 !parts[0].equals(KeyType.EXAMPLES.name().toLowerCase())) { 327 throw new RuntimeException("Wrong key type: " + parts[0]); 328 } 329 if (parts.length > HIERARCHY.length + 1) { 330 throw new RuntimeException( 331 "input key '" + keyString + "' deeper than supported hierarchy"); 332 } 333 if (parts[0].equals("data")) { 334 keyType = KeyType.DATA; 335 336 // Parses country and language info. 337 if (parts.length > 1) { 338 String substr = Util.trimToNull(parts[1]); 339 if (substr.contains(DASH_DELIM)) { 340 String[] s = substr.split(DASH_DELIM); 341 if (s.length != 2) { 342 throw new RuntimeException( 343 "Wrong format: Substring should be country " 344 + "code--language code"); 345 } 346 substr = s[0]; 347 languageCode = s[1]; 348 } 349 this.nodes.put(HIERARCHY[0], substr); 350 } 351 352 // Parses sub-country info. 353 if (parts.length > 2) { 354 for (int i = 2; i < parts.length; ++i) { 355 String substr = Util.trimToNull(parts[i]); 356 if (substr == null) { 357 break; 358 } 359 this.nodes.put(HIERARCHY[i - 1], substr); 360 } 361 } 362 } else if (parts[0].equals("examples")) { 363 keyType = KeyType.EXAMPLES; 364 365 // Parses country info. 366 if (parts.length > 1) { 367 this.nodes.put(AddressField.COUNTRY, parts[1]); 368 } 369 370 // Parses script types. 371 if (parts.length > 2) { 372 String scriptStr = parts[2]; 373 if (scriptStr.equals("local")) { 374 this.script = ScriptType.LOCAL; 375 } else if (scriptStr.equals("latin")) { 376 this.script = ScriptType.LATIN; 377 } else { 378 throw new RuntimeException("Script type has to be either latin or local."); 379 } 380 } 381 382 // Parses language code. Example: "zh_Hant" in 383 // "examples/TW/local/zH_Hant". 384 if (parts.length > 3 && !parts[3].equals(DEFAULT_LANGUAGE)) { 385 languageCode = parts[3]; 386 } 387 } 388 } 389 390 Builder setLanguageCode(String languageCode) { 391 this.languageCode = languageCode; 392 return this; 393 } 394 395 /** 396 * Sets key using {@link AddressData}. Notice that if any node in the hierarchy is empty, 397 * all the descendant nodes' values will be neglected. For example, the following address 398 * misses {@link AddressField#ADMIN_AREA}, thus its data key will be "data/US". 399 * 400 * <p> country: US<br> administrative area: null<br> locality: Mt. View </p> 401 */ 402 Builder setAddressData(AddressData data) { 403 languageCode = data.getLanguageCode(); 404 if (languageCode != null) { 405 if (Util.isExplicitLatinScript(languageCode)) { 406 script = ScriptType.LATIN; 407 } 408 } 409 410 if (data.getPostalCountry() == null) { 411 return this; 412 } 413 this.nodes.put(AddressField.COUNTRY, data.getPostalCountry()); 414 415 if (data.getAdministrativeArea() == null) { 416 return this; 417 } 418 this.nodes.put(AddressField.ADMIN_AREA, data.getAdministrativeArea()); 419 420 if (data.getLocality() == null) { 421 return this; 422 } 423 this.nodes.put(AddressField.LOCALITY, data.getLocality()); 424 425 if (data.getDependentLocality() == null) { 426 return this; 427 } 428 this.nodes.put(AddressField.DEPENDENT_LOCALITY, data.getDependentLocality()); 429 return this; 430 } 431 432 LookupKey build() { 433 return new LookupKey(this); 434 } 435 } 436 } 437