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