Home | History | Annotate | Download | only in locale
      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