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