1 /* 2 ******************************************************************************* 3 * Copyright (C) 2014, International Business Machines Corporation and 4 * others. All Rights Reserved. 5 ******************************************************************************* 6 */ 7 package com.ibm.icu.impl.locale; 8 9 import java.util.EnumSet; 10 import java.util.HashMap; 11 import java.util.HashSet; 12 import java.util.Map; 13 import java.util.MissingResourceException; 14 import java.util.Set; 15 import java.util.regex.Pattern; 16 17 import com.ibm.icu.impl.ICUResourceBundle; 18 import com.ibm.icu.util.Output; 19 import com.ibm.icu.util.UResourceBundle; 20 import com.ibm.icu.util.UResourceBundleIterator; 21 22 /** 23 */ 24 public class KeyTypeData { 25 26 private static abstract class SpecialTypeHandler { isValid(String value)27 abstract boolean isValid(String value); canonicalize(String value)28 String canonicalize(String value) { 29 return AsciiUtil.toLowerString(value); 30 } 31 } 32 33 private static class CodepointsTypeHandler extends SpecialTypeHandler { 34 private static final Pattern pat = Pattern.compile("[0-9a-fA-F]{4,6}(-[0-9a-fA-F]{4,6})*"); isValid(String value)35 boolean isValid(String value) { 36 return pat.matcher(value).matches(); 37 } 38 } 39 40 private static class ReorderCodeTypeHandler extends SpecialTypeHandler { 41 private static final Pattern pat = Pattern.compile("[a-zA-Z]{3,8}(-[a-zA-Z]{3,8})*"); isValid(String value)42 boolean isValid(String value) { 43 return pat.matcher(value).matches(); 44 } 45 } 46 47 private enum SpecialType { 48 CODEPOINTS(new CodepointsTypeHandler()), 49 REORDER_CODE(new ReorderCodeTypeHandler()); 50 51 SpecialTypeHandler handler; SpecialType(SpecialTypeHandler handler)52 SpecialType(SpecialTypeHandler handler) { 53 this.handler = handler; 54 } 55 }; 56 57 private static class KeyData { 58 String legacyId; 59 String bcpId; 60 Map<String, Type> typeMap; 61 EnumSet<SpecialType> specialTypes; 62 KeyData(String legacyId, String bcpId, Map<String, Type> typeMap, EnumSet<SpecialType> specialTypes)63 KeyData(String legacyId, String bcpId, Map<String, Type> typeMap, 64 EnumSet<SpecialType> specialTypes) { 65 this.legacyId = legacyId; 66 this.bcpId = bcpId; 67 this.typeMap = typeMap; 68 this.specialTypes = specialTypes; 69 } 70 } 71 72 private static class Type { 73 String legacyId; 74 String bcpId; 75 Type(String legacyId, String bcpId)76 Type(String legacyId, String bcpId) { 77 this.legacyId = legacyId; 78 this.bcpId = bcpId; 79 } 80 } 81 toBcpKey(String key)82 public static String toBcpKey(String key) { 83 key = AsciiUtil.toLowerString(key); 84 KeyData keyData = KEYMAP.get(key); 85 if (keyData != null) { 86 return keyData.bcpId; 87 } 88 return null; 89 } 90 toLegacyKey(String key)91 public static String toLegacyKey(String key) { 92 key = AsciiUtil.toLowerString(key); 93 KeyData keyData = KEYMAP.get(key); 94 if (keyData != null) { 95 return keyData.legacyId; 96 } 97 return null; 98 } 99 toBcpType(String key, String type, Output<Boolean> isKnownKey, Output<Boolean> isSpecialType)100 public static String toBcpType(String key, String type, 101 Output<Boolean> isKnownKey, Output<Boolean> isSpecialType) { 102 103 if (isKnownKey != null) { 104 isKnownKey.value = false; 105 } 106 if (isSpecialType != null) { 107 isSpecialType.value = false; 108 } 109 110 key = AsciiUtil.toLowerString(key); 111 type = AsciiUtil.toLowerString(type); 112 113 KeyData keyData = KEYMAP.get(key); 114 if (keyData != null) { 115 if (isKnownKey != null) { 116 isKnownKey.value = Boolean.TRUE; 117 } 118 Type t = keyData.typeMap.get(type); 119 if (t != null) { 120 return t.bcpId; 121 } 122 if (keyData.specialTypes != null) { 123 for (SpecialType st : keyData.specialTypes) { 124 if (st.handler.isValid(type)) { 125 if (isSpecialType != null) { 126 isSpecialType.value = true; 127 } 128 return st.handler.canonicalize(type); 129 } 130 } 131 } 132 } 133 return null; 134 } 135 136 toLegacyType(String key, String type, Output<Boolean> isKnownKey, Output<Boolean> isSpecialType)137 public static String toLegacyType(String key, String type, 138 Output<Boolean> isKnownKey, Output<Boolean> isSpecialType) { 139 140 if (isKnownKey != null) { 141 isKnownKey.value = false; 142 } 143 if (isSpecialType != null) { 144 isSpecialType.value = false; 145 } 146 147 key = AsciiUtil.toLowerString(key); 148 type = AsciiUtil.toLowerString(type); 149 150 KeyData keyData = KEYMAP.get(key); 151 if (keyData != null) { 152 if (isKnownKey != null) { 153 isKnownKey.value = Boolean.TRUE; 154 } 155 Type t = keyData.typeMap.get(type); 156 if (t != null) { 157 return t.legacyId; 158 } 159 if (keyData.specialTypes != null) { 160 for (SpecialType st : keyData.specialTypes) { 161 if (st.handler.isValid(type)) { 162 if (isSpecialType != null) { 163 isSpecialType.value = true; 164 } 165 return st.handler.canonicalize(type); 166 } 167 } 168 } 169 } 170 return null; 171 } 172 173 initFromResourceBundle()174 private static void initFromResourceBundle() { 175 UResourceBundle keyTypeDataRes = UResourceBundle.getBundleInstance( 176 ICUResourceBundle.ICU_BASE_NAME, 177 "keyTypeData", 178 ICUResourceBundle.ICU_DATA_CLASS_LOADER); 179 UResourceBundle keyMapRes = keyTypeDataRes.get("keyMap"); 180 UResourceBundle typeMapRes = keyTypeDataRes.get("typeMap"); 181 182 // alias data is optional 183 UResourceBundle typeAliasRes = null; 184 UResourceBundle bcpTypeAliasRes = null; 185 186 try { 187 typeAliasRes = keyTypeDataRes.get("typeAlias"); 188 } catch (MissingResourceException e) { 189 // fall through 190 } 191 192 try { 193 bcpTypeAliasRes = keyTypeDataRes.get("bcpTypeAlias"); 194 } catch (MissingResourceException e) { 195 // fall through 196 } 197 198 // iterate through keyMap resource 199 UResourceBundleIterator keyMapItr = keyMapRes.getIterator(); 200 while (keyMapItr.hasNext()) { 201 UResourceBundle keyMapEntry = keyMapItr.next(); 202 String legacyKeyId = keyMapEntry.getKey(); 203 String bcpKeyId = keyMapEntry.getString(); 204 205 boolean hasSameKey = false; 206 if (bcpKeyId.length() == 0) { 207 // Empty value indicates that BCP key is same with the legacy key. 208 bcpKeyId = legacyKeyId; 209 hasSameKey = true; 210 } 211 212 boolean isTZ = legacyKeyId.equals("timezone"); 213 214 // reverse type alias map 215 Map<String, Set<String>> typeAliasMap = null; 216 if (typeAliasRes != null) { 217 UResourceBundle typeAliasResByKey = null; 218 try { 219 typeAliasResByKey = typeAliasRes.get(legacyKeyId); 220 } catch (MissingResourceException e) { 221 // fall through 222 } 223 if (typeAliasResByKey != null) { 224 typeAliasMap = new HashMap<String, Set<String>>(); 225 UResourceBundleIterator typeAliasResItr = typeAliasResByKey.getIterator(); 226 while (typeAliasResItr.hasNext()) { 227 UResourceBundle typeAliasDataEntry = typeAliasResItr.next(); 228 String from = typeAliasDataEntry.getKey(); 229 String to = typeAliasDataEntry.getString(); 230 if (isTZ) { 231 from = from.replace(':', '/'); 232 } 233 Set<String> aliasSet = typeAliasMap.get(to); 234 if (aliasSet == null) { 235 aliasSet = new HashSet<String>(); 236 typeAliasMap.put(to, aliasSet); 237 } 238 aliasSet.add(from); 239 } 240 } 241 } 242 243 // reverse bcp type alias map 244 Map<String, Set<String>> bcpTypeAliasMap = null; 245 if (bcpTypeAliasRes != null) { 246 UResourceBundle bcpTypeAliasResByKey = null; 247 try { 248 bcpTypeAliasResByKey = bcpTypeAliasRes.get(bcpKeyId); 249 } catch (MissingResourceException e) { 250 // fall through 251 } 252 if (bcpTypeAliasResByKey != null) { 253 bcpTypeAliasMap = new HashMap<String, Set<String>>(); 254 UResourceBundleIterator bcpTypeAliasResItr = bcpTypeAliasResByKey.getIterator(); 255 while (bcpTypeAliasResItr.hasNext()) { 256 UResourceBundle bcpTypeAliasDataEntry = bcpTypeAliasResItr.next(); 257 String from = bcpTypeAliasDataEntry.getKey(); 258 String to = bcpTypeAliasDataEntry.getString(); 259 Set<String> aliasSet = bcpTypeAliasMap.get(to); 260 if (aliasSet == null) { 261 aliasSet = new HashSet<String>(); 262 bcpTypeAliasMap.put(to, aliasSet); 263 } 264 aliasSet.add(from); 265 } 266 } 267 } 268 269 Map<String, Type> typeDataMap = new HashMap<String, Type>(); 270 Set<SpecialType> specialTypeSet = null; 271 272 // look up type map for the key, and walk through the mapping data 273 UResourceBundle typeMapResByKey = null; 274 try { 275 typeMapResByKey = typeMapRes.get(legacyKeyId); 276 } catch (MissingResourceException e) { 277 // type map for each key must exist 278 assert false; 279 } 280 if (typeMapResByKey != null) { 281 UResourceBundleIterator typeMapResByKeyItr = typeMapResByKey.getIterator(); 282 while (typeMapResByKeyItr.hasNext()) { 283 UResourceBundle typeMapEntry = typeMapResByKeyItr.next(); 284 String legacyTypeId = typeMapEntry.getKey(); 285 286 // special types 287 boolean isSpecialType = false; 288 for (SpecialType st : SpecialType.values()) { 289 if (legacyTypeId.equals(st.toString())) { 290 isSpecialType = true; 291 if (specialTypeSet == null) { 292 specialTypeSet = new HashSet<SpecialType>(); 293 } 294 specialTypeSet.add(st); 295 break; 296 } 297 } 298 if (isSpecialType) { 299 continue; 300 } 301 302 if (isTZ) { 303 // a timezone key uses a colon instead of a slash in the resource. 304 // e.g. America:Los_Angeles 305 legacyTypeId = legacyTypeId.replace(':', '/'); 306 } 307 308 String bcpTypeId = typeMapEntry.getString(); 309 310 boolean hasSameType = false; 311 if (bcpTypeId.length() == 0) { 312 // Empty value indicates that BCP type is same with the legacy type. 313 bcpTypeId = legacyTypeId; 314 hasSameType = true; 315 } 316 317 // Note: legacy type value should never be 318 // equivalent to bcp type value of a different 319 // type under the same key. So we use a single 320 // map for lookup. 321 Type t = new Type(legacyTypeId, bcpTypeId); 322 typeDataMap.put(AsciiUtil.toLowerString(legacyTypeId), t); 323 if (!hasSameType) { 324 typeDataMap.put(AsciiUtil.toLowerString(bcpTypeId), t); 325 } 326 327 // Also put aliases in the map 328 if (typeAliasMap != null) { 329 Set<String> typeAliasSet = typeAliasMap.get(legacyTypeId); 330 if (typeAliasSet != null) { 331 for (String alias : typeAliasSet) { 332 typeDataMap.put(AsciiUtil.toLowerString(alias), t); 333 } 334 } 335 } 336 if (bcpTypeAliasMap != null) { 337 Set<String> bcpTypeAliasSet = bcpTypeAliasMap.get(bcpTypeId); 338 if (bcpTypeAliasSet != null) { 339 for (String alias : bcpTypeAliasSet) { 340 typeDataMap.put(AsciiUtil.toLowerString(alias), t); 341 } 342 } 343 } 344 } 345 } 346 347 EnumSet<SpecialType> specialTypes = null; 348 if (specialTypeSet != null) { 349 specialTypes = EnumSet.copyOf(specialTypeSet); 350 } 351 352 KeyData keyData = new KeyData(legacyKeyId, bcpKeyId, typeDataMap, specialTypes); 353 354 KEYMAP.put(AsciiUtil.toLowerString(legacyKeyId), keyData); 355 if (!hasSameKey) { 356 KEYMAP.put(AsciiUtil.toLowerString(bcpKeyId), keyData); 357 } 358 } 359 } 360 361 // 362 // Note: The key-type data is currently read from ICU resource bundle keyTypeData.res. 363 // In future, we may import the data into code like below directly from CLDR to 364 // avoid cyclic dependency between ULocale and UResourceBundle. For now, the code 365 // below is just for proof of concept, and commented out. 366 // 367 368 // private static final String[][] TYPE_DATA_CA = { 369 // // {<legacy type>, <bcp type - if different>}, 370 // {"buddhist", null}, 371 // {"chinese", null}, 372 // {"coptic", null}, 373 // {"dangi", null}, 374 // {"ethiopic", null}, 375 // {"ethiopic-amete-alem", "ethioaa"}, 376 // {"gregorian", "gregory"}, 377 // {"hebrew", null}, 378 // {"indian", null}, 379 // {"islamic", null}, 380 // {"islamic-civil", null}, 381 // {"islamic-rgsa", null}, 382 // {"islamic-tbla", null}, 383 // {"islamic-umalqura", null}, 384 // {"iso8601", null}, 385 // {"japanese", null}, 386 // {"persian", null}, 387 // {"roc", null}, 388 // }; 389 // 390 // private static final String[][] TYPE_DATA_KS = { 391 // // {<legacy type>, <bcp type - if different>}, 392 // {"identical", "identic"}, 393 // {"primary", "level1"}, 394 // {"quaternary", "level4"}, 395 // {"secondary", "level2"}, 396 // {"tertiary", "level3"}, 397 // }; 398 // 399 // private static final String[][] TYPE_ALIAS_KS = { 400 // // {<legacy alias>, <legacy canonical>}, 401 // {"quarternary", "quaternary"}, 402 // }; 403 // 404 // private static final String[][] BCP_TYPE_ALIAS_CA = { 405 // // {<bcp deprecated>, <bcp preferred> 406 // {"islamicc", "islamic-civil"}, 407 // }; 408 // 409 // private static final Object[][] KEY_DATA = { 410 // // {<legacy key>, <bcp key - if different>, <type map>, <type alias>, <bcp type alias>}, 411 // {"calendar", "ca", TYPE_DATA_CA, null, BCP_TYPE_ALIAS_CA}, 412 // {"colstrength", "ks", TYPE_DATA_KS, TYPE_ALIAS_KS, null}, 413 // }; 414 415 private static final Object[][] KEY_DATA = {}; 416 417 @SuppressWarnings("unused") initFromTables()418 private static void initFromTables() { 419 for (Object[] keyDataEntry : KEY_DATA) { 420 String legacyKeyId = (String)keyDataEntry[0]; 421 String bcpKeyId = (String)keyDataEntry[1]; 422 String[][] typeData = (String[][])keyDataEntry[2]; 423 String[][] typeAliasData = (String[][])keyDataEntry[3]; 424 String[][] bcpTypeAliasData = (String[][])keyDataEntry[4]; 425 426 boolean hasSameKey = false; 427 if (bcpKeyId == null) { 428 bcpKeyId = legacyKeyId; 429 hasSameKey = true; 430 } 431 432 // reverse type alias map 433 Map<String, Set<String>> typeAliasMap = null; 434 if (typeAliasData != null) { 435 typeAliasMap = new HashMap<String, Set<String>>(); 436 for (String[] typeAliasDataEntry : typeAliasData) { 437 String from = typeAliasDataEntry[0]; 438 String to = typeAliasDataEntry[1]; 439 Set<String> aliasSet = typeAliasMap.get(to); 440 if (aliasSet == null) { 441 aliasSet = new HashSet<String>(); 442 typeAliasMap.put(to, aliasSet); 443 } 444 aliasSet.add(from); 445 } 446 } 447 448 // BCP type alias map data 449 Map<String, Set<String>> bcpTypeAliasMap = null; 450 if (bcpTypeAliasData != null) { 451 bcpTypeAliasMap = new HashMap<String, Set<String>>(); 452 for (String[] bcpTypeAliasDataEntry : bcpTypeAliasData) { 453 String from = bcpTypeAliasDataEntry[0]; 454 String to = bcpTypeAliasDataEntry[1]; 455 Set<String> aliasSet = bcpTypeAliasMap.get(to); 456 if (aliasSet == null) { 457 aliasSet = new HashSet<String>(); 458 bcpTypeAliasMap.put(to, aliasSet); 459 } 460 aliasSet.add(from); 461 } 462 } 463 464 // Type map data 465 assert typeData != null; 466 Map<String, Type> typeDataMap = new HashMap<String, Type>(); 467 Set<SpecialType> specialTypeSet = null; 468 469 for (String[] typeDataEntry : typeData) { 470 String legacyTypeId = typeDataEntry[0]; 471 String bcpTypeId = typeDataEntry[1]; 472 473 // special types 474 boolean isSpecialType = false; 475 for (SpecialType st : SpecialType.values()) { 476 if (legacyTypeId.equals(st.toString())) { 477 isSpecialType = true; 478 if (specialTypeSet == null) { 479 specialTypeSet = new HashSet<SpecialType>(); 480 } 481 specialTypeSet.add(st); 482 break; 483 } 484 } 485 if (isSpecialType) { 486 continue; 487 } 488 489 boolean hasSameType = false; 490 if (bcpTypeId == null) { 491 bcpTypeId = legacyTypeId; 492 hasSameType = true; 493 } 494 495 // Note: legacy type value should never be 496 // equivalent to bcp type value of a different 497 // type under the same key. So we use a single 498 // map for lookup. 499 Type t = new Type(legacyTypeId, bcpTypeId); 500 typeDataMap.put(AsciiUtil.toLowerString(legacyTypeId), t); 501 if (!hasSameType) { 502 typeDataMap.put(AsciiUtil.toLowerString(bcpTypeId), t); 503 } 504 505 // Also put aliases in the index 506 Set<String> typeAliasSet = typeAliasMap.get(legacyTypeId); 507 if (typeAliasSet != null) { 508 for (String alias : typeAliasSet) { 509 typeDataMap.put(AsciiUtil.toLowerString(alias), t); 510 } 511 } 512 Set<String> bcpTypeAliasSet = bcpTypeAliasMap.get(bcpTypeId); 513 if (bcpTypeAliasSet != null) { 514 for (String alias : bcpTypeAliasSet) { 515 typeDataMap.put(AsciiUtil.toLowerString(alias), t); 516 } 517 } 518 } 519 520 EnumSet<SpecialType> specialTypes = null; 521 if (specialTypeSet != null) { 522 specialTypes = EnumSet.copyOf(specialTypeSet); 523 } 524 525 KeyData keyData = new KeyData(legacyKeyId, bcpKeyId, typeDataMap, specialTypes); 526 527 KEYMAP.put(AsciiUtil.toLowerString(legacyKeyId), keyData); 528 if (!hasSameKey) { 529 KEYMAP.put(AsciiUtil.toLowerString(bcpKeyId), keyData); 530 } 531 } 532 } 533 534 private static final Map<String, KeyData> KEYMAP; 535 536 static { 537 KEYMAP = new HashMap<String, KeyData>(); 538 // initFromTables(); initFromResourceBundle()539 initFromResourceBundle(); 540 } 541 542 } 543