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             // normalize separators to "_"
423             variant = variant.replaceAll(LanguageTag.SEP, BaseLocale.SEP);
424             int errIdx = checkVariants(variant, BaseLocale.SEP);
425             if (errIdx != -1) {
426                 throw new LocaleSyntaxException("Ill-formed variant: " + variant, errIdx);
427             }
428         }
429 
430         // The input locale is validated at this point.
431         // Now, updating builder's internal fields.
432         this.language = language;
433         this.script = script;
434         this.region = region;
435         this.variant = variant;
436         clearExtensions();
437 
438         Set<Character> extKeys = (localeExtensions == null) ? null : localeExtensions.getKeys();
439         if (extKeys != null) {
440             // map localeExtensions back to builder's internal format
441             for (Character key : extKeys) {
442                 Extension e = localeExtensions.getExtension(key);
443                 if (e instanceof UnicodeLocaleExtension) {
444                     UnicodeLocaleExtension ue = (UnicodeLocaleExtension)e;
445                     for (String uatr : ue.getUnicodeLocaleAttributes()) {
446                         if (uattributes == null) {
447                             uattributes = new HashSet<>(4);
448                         }
449                         uattributes.add(new CaseInsensitiveString(uatr));
450                     }
451                     for (String ukey : ue.getUnicodeLocaleKeys()) {
452                         if (ukeywords == null) {
453                             ukeywords = new HashMap<>(4);
454                         }
455                         ukeywords.put(new CaseInsensitiveString(ukey), ue.getUnicodeLocaleType(ukey));
456                     }
457                 } else {
458                     if (extensions == null) {
459                         extensions = new HashMap<>(4);
460                     }
461                     extensions.put(new CaseInsensitiveChar(key), e.getValue());
462                 }
463             }
464         }
465         return this;
466     }
467 
clear()468     public InternalLocaleBuilder clear() {
469         language = "";
470         script = "";
471         region = "";
472         variant = "";
473         clearExtensions();
474         return this;
475     }
476 
clearExtensions()477     public InternalLocaleBuilder clearExtensions() {
478         if (extensions != null) {
479             extensions.clear();
480         }
481         if (uattributes != null) {
482             uattributes.clear();
483         }
484         if (ukeywords != null) {
485             ukeywords.clear();
486         }
487         return this;
488     }
489 
getBaseLocale()490     public BaseLocale getBaseLocale() {
491         String language = this.language;
492         String script = this.script;
493         String region = this.region;
494         String variant = this.variant;
495 
496         // Special private use subtag sequence identified by "lvariant" will be
497         // interpreted as Java variant.
498         if (extensions != null) {
499             String privuse = extensions.get(PRIVATEUSE_KEY);
500             if (privuse != null) {
501                 StringTokenIterator itr = new StringTokenIterator(privuse, LanguageTag.SEP);
502                 boolean sawPrefix = false;
503                 int privVarStart = -1;
504                 while (!itr.isDone()) {
505                     if (sawPrefix) {
506                         privVarStart = itr.currentStart();
507                         break;
508                     }
509                     if (LocaleUtils.caseIgnoreMatch(itr.current(), LanguageTag.PRIVUSE_VARIANT_PREFIX)) {
510                         sawPrefix = true;
511                     }
512                     itr.next();
513                 }
514                 if (privVarStart != -1) {
515                     StringBuilder sb = new StringBuilder(variant);
516                     if (sb.length() != 0) {
517                         sb.append(BaseLocale.SEP);
518                     }
519                     sb.append(privuse.substring(privVarStart).replaceAll(LanguageTag.SEP,
520                                                                          BaseLocale.SEP));
521                     variant = sb.toString();
522                 }
523             }
524         }
525 
526         return BaseLocale.getInstance(language, script, region, variant);
527     }
528 
getLocaleExtensions()529     public LocaleExtensions getLocaleExtensions() {
530         if (LocaleUtils.isEmpty(extensions) && LocaleUtils.isEmpty(uattributes)
531             && LocaleUtils.isEmpty(ukeywords)) {
532             return null;
533         }
534 
535         LocaleExtensions lext = new LocaleExtensions(extensions, uattributes, ukeywords);
536         return lext.isEmpty() ? null : lext;
537     }
538 
539     /*
540      * Remove special private use subtag sequence identified by "lvariant"
541      * and return the rest. Only used by LocaleExtensions
542      */
removePrivateuseVariant(String privuseVal)543     static String removePrivateuseVariant(String privuseVal) {
544         StringTokenIterator itr = new StringTokenIterator(privuseVal, LanguageTag.SEP);
545 
546         // Note: privateuse value "abc-lvariant" is unchanged
547         // because no subtags after "lvariant".
548 
549         int prefixStart = -1;
550         boolean sawPrivuseVar = false;
551         while (!itr.isDone()) {
552             if (prefixStart != -1) {
553                 // Note: privateuse value "abc-lvariant" is unchanged
554                 // because no subtags after "lvariant".
555                 sawPrivuseVar = true;
556                 break;
557             }
558             if (LocaleUtils.caseIgnoreMatch(itr.current(), LanguageTag.PRIVUSE_VARIANT_PREFIX)) {
559                 prefixStart = itr.currentStart();
560             }
561             itr.next();
562         }
563         if (!sawPrivuseVar) {
564             return privuseVal;
565         }
566 
567         assert(prefixStart == 0 || prefixStart > 1);
568         return (prefixStart == 0) ? null : privuseVal.substring(0, prefixStart -1);
569     }
570 
571     /*
572      * Check if the given variant subtags separated by the given
573      * separator(s) are valid
574      */
checkVariants(String variants, String sep)575     private int checkVariants(String variants, String sep) {
576         StringTokenIterator itr = new StringTokenIterator(variants, sep);
577         while (!itr.isDone()) {
578             String s = itr.current();
579             if (!LanguageTag.isVariant(s)) {
580                 return itr.currentStart();
581             }
582             itr.next();
583         }
584         return -1;
585     }
586 
587     /*
588      * Private methods parsing Unicode Locale Extension subtags.
589      * Duplicated attributes/keywords will be ignored.
590      * The input must be a valid extension subtags (excluding singleton).
591      */
setUnicodeLocaleExtension(String subtags)592     private void setUnicodeLocaleExtension(String subtags) {
593         // wipe out existing attributes/keywords
594         if (uattributes != null) {
595             uattributes.clear();
596         }
597         if (ukeywords != null) {
598             ukeywords.clear();
599         }
600 
601         StringTokenIterator itr = new StringTokenIterator(subtags, LanguageTag.SEP);
602 
603         // parse attributes
604         while (!itr.isDone()) {
605             if (!UnicodeLocaleExtension.isAttribute(itr.current())) {
606                 break;
607             }
608             if (uattributes == null) {
609                 uattributes = new HashSet<>(4);
610             }
611             uattributes.add(new CaseInsensitiveString(itr.current()));
612             itr.next();
613         }
614 
615         // parse keywords
616         CaseInsensitiveString key = null;
617         String type;
618         int typeStart = -1;
619         int typeEnd = -1;
620         while (!itr.isDone()) {
621             if (key != null) {
622                 if (UnicodeLocaleExtension.isKey(itr.current())) {
623                     // next keyword - emit previous one
624                     assert(typeStart == -1 || typeEnd != -1);
625                     type = (typeStart == -1) ? "" : subtags.substring(typeStart, typeEnd);
626                     if (ukeywords == null) {
627                         ukeywords = new HashMap<>(4);
628                     }
629                     ukeywords.put(key, type);
630 
631                     // reset keyword info
632                     CaseInsensitiveString tmpKey = new CaseInsensitiveString(itr.current());
633                     key = ukeywords.containsKey(tmpKey) ? null : tmpKey;
634                     typeStart = typeEnd = -1;
635                 } else {
636                     if (typeStart == -1) {
637                         typeStart = itr.currentStart();
638                     }
639                     typeEnd = itr.currentEnd();
640                 }
641             } else if (UnicodeLocaleExtension.isKey(itr.current())) {
642                 // 1. first keyword or
643                 // 2. next keyword, but previous one was duplicate
644                 key = new CaseInsensitiveString(itr.current());
645                 if (ukeywords != null && ukeywords.containsKey(key)) {
646                     // duplicate
647                     key = null;
648                 }
649             }
650 
651             if (!itr.hasNext()) {
652                 if (key != null) {
653                     // last keyword
654                     assert(typeStart == -1 || typeEnd != -1);
655                     type = (typeStart == -1) ? "" : subtags.substring(typeStart, typeEnd);
656                     if (ukeywords == null) {
657                         ukeywords = new HashMap<>(4);
658                     }
659                     ukeywords.put(key, type);
660                 }
661                 break;
662             }
663 
664             itr.next();
665         }
666     }
667 
668     static final class CaseInsensitiveString {
669         private final String str, lowerStr;
670 
CaseInsensitiveString(String s)671         CaseInsensitiveString(String s) {
672             str = s;
673             lowerStr = LocaleUtils.toLowerString(s);
674         }
675 
value()676         public String value() {
677             return str;
678         }
679 
680         @Override
hashCode()681         public int hashCode() {
682             return lowerStr.hashCode();
683         }
684 
685         @Override
equals(Object obj)686         public boolean equals(Object obj) {
687             if (this == obj) {
688                 return true;
689             }
690             if (!(obj instanceof CaseInsensitiveString)) {
691                 return false;
692             }
693             return lowerStr.equals(((CaseInsensitiveString)obj).lowerStr);
694         }
695     }
696 
697     static final class CaseInsensitiveChar {
698         private final char ch, lowerCh;
699 
700         /**
701          * Constructs a CaseInsensitiveChar with the first char of the
702          * given s.
703          */
CaseInsensitiveChar(String s)704         private CaseInsensitiveChar(String s) {
705             this(s.charAt(0));
706         }
707 
CaseInsensitiveChar(char c)708         CaseInsensitiveChar(char c) {
709             ch = c;
710             lowerCh = LocaleUtils.toLower(ch);
711         }
712 
value()713         public char value() {
714             return ch;
715         }
716 
717         @Override
hashCode()718         public int hashCode() {
719             return lowerCh;
720         }
721 
722         @Override
equals(Object obj)723         public boolean equals(Object obj) {
724             if (this == obj) {
725                 return true;
726             }
727             if (!(obj instanceof CaseInsensitiveChar)) {
728                 return false;
729             }
730             return lowerCh == ((CaseInsensitiveChar)obj).lowerCh;
731         }
732     }
733 }
734