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