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