1 /* GENERATED SOURCE. DO NOT MODIFY. */ 2 // 2016 and later: Unicode, Inc. and others. 3 // License & terms of use: http://www.unicode.org/copyright.html#License 4 /* 5 ******************************************************************************* 6 * Copyright (C) 2009-2010, International Business Machines Corporation and * 7 * others. All Rights Reserved. * 8 ******************************************************************************* 9 */ 10 package android.icu.impl.locale; 11 12 import java.util.ArrayList; 13 import java.util.HashMap; 14 import java.util.HashSet; 15 import java.util.List; 16 import java.util.Set; 17 18 /** 19 * @hide Only a subset of ICU is exposed in Android 20 */ 21 public final class InternalLocaleBuilder { 22 23 private static final boolean JDKIMPL = false; 24 25 private String _language = ""; 26 private String _script = ""; 27 private String _region = ""; 28 private String _variant = ""; 29 30 private static final CaseInsensitiveChar PRIVUSE_KEY = new CaseInsensitiveChar(LanguageTag.PRIVATEUSE.charAt(0)); 31 32 private HashMap<CaseInsensitiveChar, String> _extensions; 33 private HashSet<CaseInsensitiveString> _uattributes; 34 private HashMap<CaseInsensitiveString, String> _ukeywords; 35 36 37 public InternalLocaleBuilder() { 38 } 39 40 public InternalLocaleBuilder setLanguage(String language) throws LocaleSyntaxException { 41 if (language == null || language.length() == 0) { 42 _language = ""; 43 } else { 44 if (!LanguageTag.isLanguage(language)) { 45 throw new LocaleSyntaxException("Ill-formed language: " + language, 0); 46 } 47 _language = language; 48 } 49 return this; 50 } 51 52 public InternalLocaleBuilder setScript(String script) throws LocaleSyntaxException { 53 if (script == null || script.length() == 0) { 54 _script = ""; 55 } else { 56 if (!LanguageTag.isScript(script)) { 57 throw new LocaleSyntaxException("Ill-formed script: " + script, 0); 58 } 59 _script = script; 60 } 61 return this; 62 } 63 64 public InternalLocaleBuilder setRegion(String region) throws LocaleSyntaxException { 65 if (region == null || region.length() == 0) { 66 _region = ""; 67 } else { 68 if (!LanguageTag.isRegion(region)) { 69 throw new LocaleSyntaxException("Ill-formed region: " + region, 0); 70 } 71 _region = region; 72 } 73 return this; 74 } 75 76 public InternalLocaleBuilder setVariant(String variant) throws LocaleSyntaxException { 77 if (variant == null || variant.length() == 0) { 78 _variant = ""; 79 } else { 80 // normalize separators to "_" 81 String var = variant.replaceAll(LanguageTag.SEP, BaseLocale.SEP); 82 int errIdx = checkVariants(var, BaseLocale.SEP); 83 if (errIdx != -1) { 84 throw new LocaleSyntaxException("Ill-formed variant: " + variant, errIdx); 85 } 86 _variant = var; 87 } 88 return this; 89 } 90 91 public InternalLocaleBuilder addUnicodeLocaleAttribute(String attribute) throws LocaleSyntaxException { 92 if (attribute == null || !UnicodeLocaleExtension.isAttribute(attribute)) { 93 throw new LocaleSyntaxException("Ill-formed Unicode locale attribute: " + attribute); 94 } 95 // Use case insensitive string to prevent duplication 96 if (_uattributes == null) { 97 _uattributes = new HashSet<CaseInsensitiveString>(4); 98 } 99 _uattributes.add(new CaseInsensitiveString(attribute)); 100 return this; 101 } 102 103 public InternalLocaleBuilder removeUnicodeLocaleAttribute(String attribute) throws LocaleSyntaxException { 104 if (attribute == null || !UnicodeLocaleExtension.isAttribute(attribute)) { 105 throw new LocaleSyntaxException("Ill-formed Unicode locale attribute: " + attribute); 106 } 107 if (_uattributes != null) { 108 _uattributes.remove(new CaseInsensitiveString(attribute)); 109 } 110 return this; 111 } 112 113 public InternalLocaleBuilder setUnicodeLocaleKeyword(String key, String type) throws LocaleSyntaxException { 114 if (!UnicodeLocaleExtension.isKey(key)) { 115 throw new LocaleSyntaxException("Ill-formed Unicode locale keyword key: " + key); 116 } 117 118 CaseInsensitiveString cikey = new CaseInsensitiveString(key); 119 if (type == null) { 120 if (_ukeywords != null) { 121 // null type is used for remove the key 122 _ukeywords.remove(cikey); 123 } 124 } else { 125 if (type.length() != 0) { 126 // normalize separator to "-" 127 String tp = type.replaceAll(BaseLocale.SEP, LanguageTag.SEP); 128 // validate 129 StringTokenIterator itr = new StringTokenIterator(tp, LanguageTag.SEP); 130 while (!itr.isDone()) { 131 String s = itr.current(); 132 if (!UnicodeLocaleExtension.isTypeSubtag(s)) { 133 throw new LocaleSyntaxException("Ill-formed Unicode locale keyword type: " + type, itr.currentStart()); 134 } 135 itr.next(); 136 } 137 } 138 if (_ukeywords == null) { 139 _ukeywords = new HashMap<CaseInsensitiveString, String>(4); 140 } 141 _ukeywords.put(cikey, type); 142 } 143 return this; 144 } 145 146 public InternalLocaleBuilder setExtension(char singleton, String value) throws LocaleSyntaxException { 147 // validate key 148 boolean isBcpPrivateuse = LanguageTag.isPrivateusePrefixChar(singleton); 149 if (!isBcpPrivateuse && !LanguageTag.isExtensionSingletonChar(singleton)) { 150 throw new LocaleSyntaxException("Ill-formed extension key: " + singleton); 151 } 152 153 boolean remove = (value == null || value.length() == 0); 154 CaseInsensitiveChar key = new CaseInsensitiveChar(singleton); 155 156 if (remove) { 157 if (UnicodeLocaleExtension.isSingletonChar(key.value())) { 158 // clear entire Unicode locale extension 159 if (_uattributes != null) { 160 _uattributes.clear(); 161 } 162 if (_ukeywords != null) { 163 _ukeywords.clear(); 164 } 165 } else { 166 if (_extensions != null && _extensions.containsKey(key)) { 167 _extensions.remove(key); 168 } 169 } 170 } else { 171 // validate value 172 String val = value.replaceAll(BaseLocale.SEP, LanguageTag.SEP); 173 StringTokenIterator itr = new StringTokenIterator(val, LanguageTag.SEP); 174 while (!itr.isDone()) { 175 String s = itr.current(); 176 boolean validSubtag; 177 if (isBcpPrivateuse) { 178 validSubtag = LanguageTag.isPrivateuseSubtag(s); 179 } else { 180 validSubtag = LanguageTag.isExtensionSubtag(s); 181 } 182 if (!validSubtag) { 183 throw new LocaleSyntaxException("Ill-formed extension value: " + s, itr.currentStart()); 184 } 185 itr.next(); 186 } 187 188 if (UnicodeLocaleExtension.isSingletonChar(key.value())) { 189 setUnicodeLocaleExtension(val); 190 } else { 191 if (_extensions == null) { 192 _extensions = new HashMap<CaseInsensitiveChar, String>(4); 193 } 194 _extensions.put(key, val); 195 } 196 } 197 return this; 198 } 199 200 /* 201 * Set extension/private subtags in a single string representation 202 */ 203 public InternalLocaleBuilder setExtensions(String subtags) throws LocaleSyntaxException { 204 if (subtags == null || subtags.length() == 0) { 205 clearExtensions(); 206 return this; 207 } 208 subtags = subtags.replaceAll(BaseLocale.SEP, LanguageTag.SEP); 209 StringTokenIterator itr = new StringTokenIterator(subtags, LanguageTag.SEP); 210 211 List<String> extensions = null; 212 String privateuse = null; 213 214 int parsed = 0; 215 int start; 216 217 // Make a list of extension subtags 218 while (!itr.isDone()) { 219 String s = itr.current(); 220 if (LanguageTag.isExtensionSingleton(s)) { 221 start = itr.currentStart(); 222 String singleton = s; 223 StringBuilder sb = new StringBuilder(singleton); 224 225 itr.next(); 226 while (!itr.isDone()) { 227 s = itr.current(); 228 if (LanguageTag.isExtensionSubtag(s)) { 229 sb.append(LanguageTag.SEP).append(s); 230 parsed = itr.currentEnd(); 231 } else { 232 break; 233 } 234 itr.next(); 235 } 236 237 if (parsed < start) { 238 throw new LocaleSyntaxException("Incomplete extension '" + singleton + "'", start); 239 } 240 241 if (extensions == null) { 242 extensions = new ArrayList<String>(4); 243 } 244 extensions.add(sb.toString()); 245 } else { 246 break; 247 } 248 } 249 if (!itr.isDone()) { 250 String s = itr.current(); 251 if (LanguageTag.isPrivateusePrefix(s)) { 252 start = itr.currentStart(); 253 StringBuilder sb = new StringBuilder(s); 254 255 itr.next(); 256 while (!itr.isDone()) { 257 s = itr.current(); 258 if (!LanguageTag.isPrivateuseSubtag(s)) { 259 break; 260 } 261 sb.append(LanguageTag.SEP).append(s); 262 parsed = itr.currentEnd(); 263 264 itr.next(); 265 } 266 if (parsed <= start) { 267 throw new LocaleSyntaxException("Incomplete privateuse:" + subtags.substring(start), start); 268 } else { 269 privateuse = sb.toString(); 270 } 271 } 272 } 273 274 if (!itr.isDone()) { 275 throw new LocaleSyntaxException("Ill-formed extension subtags:" + subtags.substring(itr.currentStart()), itr.currentStart()); 276 } 277 278 return setExtensions(extensions, privateuse); 279 } 280 281 /* 282 * Set a list of BCP47 extensions and private use subtags 283 * BCP47 extensions are already validated and well-formed, but may contain duplicates 284 */ 285 private InternalLocaleBuilder setExtensions(List<String> bcpExtensions, String privateuse) { 286 clearExtensions(); 287 288 if (bcpExtensions != null && bcpExtensions.size() > 0) { 289 HashSet<CaseInsensitiveChar> processedExtensions = new HashSet<CaseInsensitiveChar>(bcpExtensions.size()); 290 for (String bcpExt : bcpExtensions) { 291 CaseInsensitiveChar key = new CaseInsensitiveChar(bcpExt.charAt(0)); 292 // ignore duplicates 293 if (!processedExtensions.contains(key)) { 294 // each extension string contains singleton, e.g. "a-abc-def" 295 if (UnicodeLocaleExtension.isSingletonChar(key.value())) { 296 setUnicodeLocaleExtension(bcpExt.substring(2)); 297 } else { 298 if (_extensions == null) { 299 _extensions = new HashMap<CaseInsensitiveChar, String>(4); 300 } 301 _extensions.put(key, bcpExt.substring(2)); 302 } 303 } 304 } 305 } 306 if (privateuse != null && privateuse.length() > 0) { 307 // privateuse string contains prefix, e.g. "x-abc-def" 308 if (_extensions == null) { 309 _extensions = new HashMap<CaseInsensitiveChar, String>(1); 310 } 311 _extensions.put(new CaseInsensitiveChar(privateuse.charAt(0)), privateuse.substring(2)); 312 } 313 314 return this; 315 } 316 317 /* 318 * Reset Builder's internal state with the given language tag 319 */ 320 public InternalLocaleBuilder setLanguageTag(LanguageTag langtag) { 321 clear(); 322 if (langtag.getExtlangs().size() > 0) { 323 _language = langtag.getExtlangs().get(0); 324 } else { 325 String language = langtag.getLanguage(); 326 if (!language.equals(LanguageTag.UNDETERMINED)) { 327 _language = language; 328 } 329 } 330 _script = langtag.getScript(); 331 _region = langtag.getRegion(); 332 333 List<String> bcpVariants = langtag.getVariants(); 334 if (bcpVariants.size() > 0) { 335 StringBuilder var = new StringBuilder(bcpVariants.get(0)); 336 for (int i = 1; i < bcpVariants.size(); i++) { 337 var.append(BaseLocale.SEP).append(bcpVariants.get(i)); 338 } 339 _variant = var.toString(); 340 } 341 342 setExtensions(langtag.getExtensions(), langtag.getPrivateuse()); 343 344 return this; 345 } 346 347 public InternalLocaleBuilder setLocale(BaseLocale base, LocaleExtensions extensions) throws LocaleSyntaxException { 348 String language = base.getLanguage(); 349 String script = base.getScript(); 350 String region = base.getRegion(); 351 String variant = base.getVariant(); 352 353 if (JDKIMPL) { 354 // Special backward compatibility support 355 356 // Exception 1 - ja_JP_JP 357 if (language.equals("ja") && region.equals("JP") && variant.equals("JP")) { 358 // When locale ja_JP_JP is created, ca-japanese is always there. 359 // The builder ignores the variant "JP" 360 assert("japanese".equals(extensions.getUnicodeLocaleType("ca"))); 361 variant = ""; 362 } 363 // Exception 2 - th_TH_TH 364 else if (language.equals("th") && region.equals("TH") && variant.equals("TH")) { 365 // When locale th_TH_TH is created, nu-thai is always there. 366 // The builder ignores the variant "TH" 367 assert("thai".equals(extensions.getUnicodeLocaleType("nu"))); 368 variant = ""; 369 } 370 // Exception 3 - no_NO_NY 371 else if (language.equals("no") && region.equals("NO") && variant.equals("NY")) { 372 // no_NO_NY is a valid locale and used by Java 6 or older versions. 373 // The build ignores the variant "NY" and change the language to "nn". 374 language = "nn"; 375 variant = ""; 376 } 377 } 378 379 // Validate base locale fields before updating internal state. 380 // LocaleExtensions always store validated/canonicalized values, 381 // so no checks are necessary. 382 if (language.length() > 0 && !LanguageTag.isLanguage(language)) { 383 throw new LocaleSyntaxException("Ill-formed language: " + language); 384 } 385 386 if (script.length() > 0 && !LanguageTag.isScript(script)) { 387 throw new LocaleSyntaxException("Ill-formed script: " + script); 388 } 389 390 if (region.length() > 0 && !LanguageTag.isRegion(region)) { 391 throw new LocaleSyntaxException("Ill-formed region: " + region); 392 } 393 394 if (variant.length() > 0) { 395 int errIdx = checkVariants(variant, BaseLocale.SEP); 396 if (errIdx != -1) { 397 throw new LocaleSyntaxException("Ill-formed variant: " + variant, errIdx); 398 } 399 } 400 401 // The input locale is validated at this point. 402 // Now, updating builder's internal fields. 403 _language = language; 404 _script = script; 405 _region = region; 406 _variant = variant; 407 clearExtensions(); 408 409 Set<Character> extKeys = (extensions == null) ? null : extensions.getKeys(); 410 if (extKeys != null) { 411 // map extensions back to builder's internal format 412 for (Character key : extKeys) { 413 Extension e = extensions.getExtension(key); 414 if (e instanceof UnicodeLocaleExtension) { 415 UnicodeLocaleExtension ue = (UnicodeLocaleExtension)e; 416 for (String uatr : ue.getUnicodeLocaleAttributes()) { 417 if (_uattributes == null) { 418 _uattributes = new HashSet<CaseInsensitiveString>(4); 419 } 420 _uattributes.add(new CaseInsensitiveString(uatr)); 421 } 422 for (String ukey : ue.getUnicodeLocaleKeys()) { 423 if (_ukeywords == null) { 424 _ukeywords = new HashMap<CaseInsensitiveString, String>(4); 425 } 426 _ukeywords.put(new CaseInsensitiveString(ukey), ue.getUnicodeLocaleType(ukey)); 427 } 428 } else { 429 if (_extensions == null) { 430 _extensions = new HashMap<CaseInsensitiveChar, String>(4); 431 } 432 _extensions.put(new CaseInsensitiveChar(key.charValue()), e.getValue()); 433 } 434 } 435 } 436 return this; 437 } 438 439 public InternalLocaleBuilder clear() { 440 _language = ""; 441 _script = ""; 442 _region = ""; 443 _variant = ""; 444 clearExtensions(); 445 return this; 446 } 447 448 public InternalLocaleBuilder clearExtensions() { 449 if (_extensions != null) { 450 _extensions.clear(); 451 } 452 if (_uattributes != null) { 453 _uattributes.clear(); 454 } 455 if (_ukeywords != null) { 456 _ukeywords.clear(); 457 } 458 return this; 459 } 460 461 public BaseLocale getBaseLocale() { 462 String language = _language; 463 String script = _script; 464 String region = _region; 465 String variant = _variant; 466 467 // Special private use subtag sequence identified by "lvariant" will be 468 // interpreted as Java variant. 469 if (_extensions != null) { 470 String privuse = _extensions.get(PRIVUSE_KEY); 471 if (privuse != null) { 472 StringTokenIterator itr = new StringTokenIterator(privuse, LanguageTag.SEP); 473 boolean sawPrefix = false; 474 int privVarStart = -1; 475 while (!itr.isDone()) { 476 if (sawPrefix) { 477 privVarStart = itr.currentStart(); 478 break; 479 } 480 if (AsciiUtil.caseIgnoreMatch(itr.current(), LanguageTag.PRIVUSE_VARIANT_PREFIX)) { 481 sawPrefix = true; 482 } 483 itr.next(); 484 } 485 if (privVarStart != -1) { 486 StringBuilder sb = new StringBuilder(variant); 487 if (sb.length() != 0) { 488 sb.append(BaseLocale.SEP); 489 } 490 sb.append(privuse.substring(privVarStart).replaceAll(LanguageTag.SEP, BaseLocale.SEP)); 491 variant = sb.toString(); 492 } 493 } 494 } 495 496 return BaseLocale.getInstance(language, script, region, variant); 497 } 498 499 public LocaleExtensions getLocaleExtensions() { 500 if ((_extensions == null || _extensions.size() == 0) 501 && (_uattributes == null || _uattributes.size() == 0) 502 && (_ukeywords == null || _ukeywords.size() == 0)) { 503 return LocaleExtensions.EMPTY_EXTENSIONS; 504 } 505 506 return new LocaleExtensions(_extensions, _uattributes, _ukeywords); 507 } 508 509 /* 510 * Remove special private use subtag sequence identified by "lvariant" 511 * and return the rest. Only used by LocaleExtensions 512 */ 513 static String removePrivateuseVariant(String privuseVal) { 514 StringTokenIterator itr = new StringTokenIterator(privuseVal, LanguageTag.SEP); 515 516 // Note: privateuse value "abc-lvariant" is unchanged 517 // because no subtags after "lvariant". 518 519 int prefixStart = -1; 520 boolean sawPrivuseVar = false; 521 while (!itr.isDone()) { 522 if (prefixStart != -1) { 523 // Note: privateuse value "abc-lvariant" is unchanged 524 // because no subtags after "lvariant". 525 sawPrivuseVar = true; 526 break; 527 } 528 if (AsciiUtil.caseIgnoreMatch(itr.current(), LanguageTag.PRIVUSE_VARIANT_PREFIX)) { 529 prefixStart = itr.currentStart(); 530 } 531 itr.next(); 532 } 533 if (!sawPrivuseVar) { 534 return privuseVal; 535 } 536 537 assert(prefixStart == 0 || prefixStart > 1); 538 return (prefixStart == 0) ? null : privuseVal.substring(0, prefixStart -1); 539 } 540 541 /* 542 * Check if the given variant subtags separated by the given 543 * separator(s) are valid 544 */ 545 private int checkVariants(String variants, String sep) { 546 StringTokenIterator itr = new StringTokenIterator(variants, sep); 547 while (!itr.isDone()) { 548 String s = itr.current(); 549 if (!LanguageTag.isVariant(s)) { 550 return itr.currentStart(); 551 } 552 itr.next(); 553 } 554 return -1; 555 } 556 557 /* 558 * Private methods parsing Unicode Locale Extension subtags. 559 * Duplicated attributes/keywords will be ignored. 560 * The input must be a valid extension subtags (excluding singleton). 561 */ 562 private void setUnicodeLocaleExtension(String subtags) { 563 // wipe out existing attributes/keywords 564 if (_uattributes != null) { 565 _uattributes.clear(); 566 } 567 if (_ukeywords != null) { 568 _ukeywords.clear(); 569 } 570 571 StringTokenIterator itr = new StringTokenIterator(subtags, LanguageTag.SEP); 572 573 // parse attributes 574 while (!itr.isDone()) { 575 if (!UnicodeLocaleExtension.isAttribute(itr.current())) { 576 break; 577 } 578 if (_uattributes == null) { 579 _uattributes = new HashSet<CaseInsensitiveString>(4); 580 } 581 _uattributes.add(new CaseInsensitiveString(itr.current())); 582 itr.next(); 583 } 584 585 // parse keywords 586 CaseInsensitiveString key = null; 587 String type; 588 int typeStart = -1; 589 int typeEnd = -1; 590 while (!itr.isDone()) { 591 if (key != null) { 592 if (UnicodeLocaleExtension.isKey(itr.current())) { 593 // next keyword - emit previous one 594 assert(typeStart == -1 || typeEnd != -1); 595 type = (typeStart == -1) ? "" : subtags.substring(typeStart, typeEnd); 596 if (_ukeywords == null) { 597 _ukeywords = new HashMap<CaseInsensitiveString, String>(4); 598 } 599 _ukeywords.put(key, type); 600 601 // reset keyword info 602 CaseInsensitiveString tmpKey = new CaseInsensitiveString(itr.current()); 603 key = _ukeywords.containsKey(tmpKey) ? null : tmpKey; 604 typeStart = typeEnd = -1; 605 } else { 606 if (typeStart == -1) { 607 typeStart = itr.currentStart(); 608 } 609 typeEnd = itr.currentEnd(); 610 } 611 } else if (UnicodeLocaleExtension.isKey(itr.current())) { 612 // 1. first keyword or 613 // 2. next keyword, but previous one was duplicate 614 key = new CaseInsensitiveString(itr.current()); 615 if (_ukeywords != null && _ukeywords.containsKey(key)) { 616 // duplicate 617 key = null; 618 } 619 } 620 621 if (!itr.hasNext()) { 622 if (key != null) { 623 // last keyword 624 assert(typeStart == -1 || typeEnd != -1); 625 type = (typeStart == -1) ? "" : subtags.substring(typeStart, typeEnd); 626 if (_ukeywords == null) { 627 _ukeywords = new HashMap<CaseInsensitiveString, String>(4); 628 } 629 _ukeywords.put(key, type); 630 } 631 break; 632 } 633 634 itr.next(); 635 } 636 } 637 638 static class CaseInsensitiveString { 639 private String _s; 640 641 CaseInsensitiveString(String s) { 642 _s = s; 643 } 644 645 public String value() { 646 return _s; 647 } 648 649 @Override 650 public int hashCode() { 651 return AsciiUtil.toLowerString(_s).hashCode(); 652 } 653 654 @Override 655 public boolean equals(Object obj) { 656 if (this == obj) { 657 return true; 658 } 659 if (!(obj instanceof CaseInsensitiveString)) { 660 return false; 661 } 662 return AsciiUtil.caseIgnoreMatch(_s, ((CaseInsensitiveString)obj).value()); 663 } 664 } 665 666 static class CaseInsensitiveChar { 667 private char _c; 668 669 CaseInsensitiveChar(char c) { 670 _c = c; 671 } 672 673 public char value() { 674 return _c; 675 } 676 677 @Override 678 public int hashCode() { 679 return AsciiUtil.toLower(_c); 680 } 681 682 @Override 683 public boolean equals(Object obj) { 684 if (this == obj) { 685 return true; 686 } 687 if (!(obj instanceof CaseInsensitiveChar)) { 688 return false; 689 } 690 return _c == AsciiUtil.toLower(((CaseInsensitiveChar)obj).value()); 691 } 692 693 } 694 } 695