1 /*
2  * Copyright (c) 2012, 2019, 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 package build.tools.cldrconverter;
27 
28 import java.io.File;
29 import java.io.IOException;
30 import java.text.DateFormatSymbols;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.Set;
39 import org.xml.sax.Attributes;
40 import org.xml.sax.InputSource;
41 import org.xml.sax.SAXException;
42 
43 /**
44  * Handles parsing of files in Locale Data Markup Language and produces a map
45  * that uses the keys and values of JRE locale data.
46  */
47 class LDMLParseHandler extends AbstractLDMLHandler<Object> {
48     private String defaultNumberingSystem;
49     private String currentNumberingSystem = "";
50     private CalendarType currentCalendarType;
51     private String zoneNameStyle; // "long" or "short" for time zone names
52     private String zonePrefix;
53     private final String id;
54     private String currentContext = ""; // "format"/"stand-alone"
55     private String currentWidth = ""; // "wide"/"narrow"/"abbreviated"
56     private String currentStyle = ""; // short, long for decimalFormat
57 
LDMLParseHandler(String id)58     LDMLParseHandler(String id) {
59         this.id = id;
60     }
61 
62     @Override
resolveEntity(String publicID, String systemID)63     public InputSource resolveEntity(String publicID, String systemID) throws IOException, SAXException {
64         // avoid HTTP traffic to unicode.org
65         if (systemID.startsWith(CLDRConverter.LDML_DTD_SYSTEM_ID)) {
66             return new InputSource((new File(CLDRConverter.LOCAL_LDML_DTD)).toURI().toString());
67         }
68         return null;
69     }
70 
71     @Override
startElement(String uri, String localName, String qName, Attributes attributes)72     public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
73         switch (qName) {
74         //
75         // Generic information
76         //
77         case "identity":
78             // ignore this element - it has language and territory elements that aren't locale data
79             pushIgnoredContainer(qName);
80             break;
81 
82         // for LocaleNames
83         // copy string
84         case "localeSeparator":
85             pushStringEntry(qName, attributes,
86                 CLDRConverter.LOCALE_SEPARATOR);
87             break;
88         case "localeKeyTypePattern":
89             pushStringEntry(qName, attributes,
90                 CLDRConverter.LOCALE_KEYTYPE);
91             break;
92 
93         case "language":
94         case "script":
95         case "territory":
96         case "variant":
97             // for LocaleNames
98             // copy string
99             pushStringEntry(qName, attributes,
100                 CLDRConverter.LOCALE_NAME_PREFIX +
101                 (qName.equals("variant") ? "%%" : "") +
102                 attributes.getValue("type"));
103             break;
104 
105         case "key":
106             // for LocaleNames
107             // copy string
108             {
109                 String key = convertOldKeyName(attributes.getValue("type"));
110                 if (key.length() == 2) {
111                     pushStringEntry(qName, attributes,
112                         CLDRConverter.LOCALE_KEY_PREFIX + key);
113                 } else {
114                     pushIgnoredContainer(qName);
115                 }
116             }
117             break;
118 
119         case "type":
120             // for LocaleNames/CalendarNames
121             // copy string
122             {
123                 String key = convertOldKeyName(attributes.getValue("key"));
124                 if (key.length() == 2) {
125                     pushStringEntry(qName, attributes,
126                     CLDRConverter.LOCALE_TYPE_PREFIX + key + "." +
127                     attributes.getValue("type"));
128                 } else {
129                     pushIgnoredContainer(qName);
130                 }
131             }
132             break;
133 
134         //
135         // Currency information
136         //
137         case "currency":
138             // for CurrencyNames
139             // stash away "type" value for nested <symbol>
140             pushKeyContainer(qName, attributes, attributes.getValue("type"));
141             break;
142         case "symbol":
143             // for CurrencyNames
144             // need to get the key from the containing <currency> element
145             pushStringEntry(qName, attributes, CLDRConverter.CURRENCY_SYMBOL_PREFIX
146                                                + getContainerKey());
147             break;
148 
149         // Calendar or currency
150         case "displayName":
151             {
152                 if (currentContainer.getqName().equals("field")) {
153                     pushStringEntry(qName, attributes,
154                             (currentCalendarType != null ? currentCalendarType.keyElementName() : "")
155                             + "field." + getContainerKey());
156                 } else {
157                     // for CurrencyNames
158                     // need to get the key from the containing <currency> element
159                     // ignore if is has "count" attribute
160                     String containerKey = getContainerKey();
161                     if (containerKey != null && attributes.getValue("count") == null) {
162                         pushStringEntry(qName, attributes,
163                                         CLDRConverter.CURRENCY_NAME_PREFIX
164                                         + containerKey.toLowerCase(Locale.ROOT),
165                                         attributes.getValue("type"));
166                     } else {
167                         pushIgnoredContainer(qName);
168                     }
169                 }
170             }
171             break;
172 
173         //
174         // Calendar information
175         //
176         case "calendar":
177             {
178                 // mostly for FormatData (CalendarData items firstDay and minDays are also nested)
179                 // use only if it's supported by java.util.Calendar.
180                 String calendarName = attributes.getValue("type");
181                 currentCalendarType = CalendarType.forName(calendarName);
182                 if (currentCalendarType != null) {
183                     pushContainer(qName, attributes);
184                 } else {
185                     pushIgnoredContainer(qName);
186                 }
187             }
188             break;
189         case "fields":
190             {
191                 pushContainer(qName, attributes);
192             }
193             break;
194         case "field":
195             {
196                 String type = attributes.getValue("type");
197                 switch (type) {
198                 case "era":
199                 case "year":
200                 case "month":
201                 case "week":
202                 case "weekday":
203                 case "dayperiod":
204                 case "hour":
205                 case "minute":
206                 case "second":
207                 case "zone":
208                     pushKeyContainer(qName, attributes, type);
209                     break;
210                 default:
211                     pushIgnoredContainer(qName);
212                     break;
213                 }
214             }
215             break;
216         case "monthContext":
217             {
218                 // for FormatData
219                 // need to keep stand-alone and format, to allow for inheritance in CLDR
220                 String type = attributes.getValue("type");
221                 if ("stand-alone".equals(type) || "format".equals(type)) {
222                     currentContext = type;
223                     pushKeyContainer(qName, attributes, type);
224                 } else {
225                     pushIgnoredContainer(qName);
226                 }
227             }
228             break;
229         case "monthWidth":
230             {
231                 // for FormatData
232                 // create string array for the two types that the JRE knows
233                 // keep info about the context type so we can sort out inheritance later
234                 if (currentCalendarType == null) {
235                     pushIgnoredContainer(qName);
236                     break;
237                 }
238                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
239                 currentWidth = attributes.getValue("type");
240                 switch (currentWidth) {
241                 case "wide":
242                     pushStringArrayEntry(qName, attributes, prefix + "MonthNames/" + getContainerKey(), 13);
243                     break;
244                 case "abbreviated":
245                     pushStringArrayEntry(qName, attributes, prefix + "MonthAbbreviations/" + getContainerKey(), 13);
246                     break;
247                 case "narrow":
248                     pushStringArrayEntry(qName, attributes, prefix + "MonthNarrows/" + getContainerKey(), 13);
249                     break;
250                 default:
251                     pushIgnoredContainer(qName);
252                     break;
253                 }
254             }
255             break;
256         case "month":
257             // for FormatData
258             // add to string array entry of monthWidth element
259             pushStringArrayElement(qName, attributes, Integer.parseInt(attributes.getValue("type")) - 1);
260             break;
261         case "dayContext":
262             {
263                 // for FormatData
264                 // need to keep stand-alone and format, to allow for multiple inheritance in CLDR
265                 String type = attributes.getValue("type");
266                 if ("stand-alone".equals(type) || "format".equals(type)) {
267                     currentContext = type;
268                     pushKeyContainer(qName, attributes, type);
269                 } else {
270                     pushIgnoredContainer(qName);
271                 }
272             }
273             break;
274         case "dayWidth":
275             {
276                 // for FormatData
277                 // create string array for the two types that the JRE knows
278                 // keep info about the context type so we can sort out inheritance later
279                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
280                 currentWidth = attributes.getValue("type");
281                 switch (currentWidth) {
282                 case "wide":
283                     pushStringArrayEntry(qName, attributes, prefix + "DayNames/" + getContainerKey(), 7);
284                     break;
285                 case "abbreviated":
286                     pushStringArrayEntry(qName, attributes, prefix + "DayAbbreviations/" + getContainerKey(), 7);
287                     break;
288                 case "narrow":
289                     pushStringArrayEntry(qName, attributes, prefix + "DayNarrows/" + getContainerKey(), 7);
290                     break;
291                 default:
292                     pushIgnoredContainer(qName);
293                     break;
294                 }
295             }
296             break;
297         case "day":
298             // for FormatData
299             // add to string array entry of monthWidth element
300             pushStringArrayElement(qName, attributes, Integer.parseInt(DAY_OF_WEEK_MAP.get(attributes.getValue("type"))) - 1);
301             break;
302         case "dayPeriodContext":
303             // for FormatData
304             // need to keep stand-alone and format, to allow for multiple inheritance in CLDR
305             // for FormatData
306             // need to keep stand-alone and format, to allow for multiple inheritance in CLDR
307             {
308                 String type = attributes.getValue("type");
309                 if ("stand-alone".equals(type) || "format".equals(type)) {
310                     currentContext = type;
311                     pushKeyContainer(qName, attributes, type);
312                 } else {
313                     pushIgnoredContainer(qName);
314                 }
315             }
316             break;
317         case "dayPeriodWidth":
318             // for FormatData
319             // create string array entry for am/pm. only keeping wide
320             currentWidth = attributes.getValue("type");
321             switch (currentWidth) {
322             case "wide":
323                 pushStringArrayEntry(qName, attributes, "AmPmMarkers/" + getContainerKey(), 2);
324                 break;
325             case "narrow":
326                 pushStringArrayEntry(qName, attributes, "narrow.AmPmMarkers/" + getContainerKey(), 2);
327                 break;
328             case "abbreviated":
329                 pushStringArrayEntry(qName, attributes, "abbreviated.AmPmMarkers/" + getContainerKey(), 2);
330                 break;
331             default:
332                 pushIgnoredContainer(qName);
333                 break;
334             }
335             break;
336         case "dayPeriod":
337             // for FormatData
338             // add to string array entry of AmPmMarkers element
339             if (attributes.getValue("alt") == null) {
340                 switch (attributes.getValue("type")) {
341                 case "am":
342                     pushStringArrayElement(qName, attributes, 0);
343                     break;
344                 case "pm":
345                     pushStringArrayElement(qName, attributes, 1);
346                     break;
347                 default:
348                     pushIgnoredContainer(qName);
349                     break;
350                 }
351             } else {
352                 // discard alt values
353                 pushIgnoredContainer(qName);
354             }
355             break;
356         case "eraNames":
357             // CLDR era names are inconsistent in terms of their lengths. For example,
358             // the full names of Japanese imperial eras are eraAbbr, while the full names
359             // of the Julian eras are eraNames.
360             if (currentCalendarType == null) {
361                 assert currentContainer instanceof IgnoredContainer;
362                 pushIgnoredContainer(qName);
363             } else {
364                 String key = currentCalendarType.keyElementName() + "long.Eras"; // for now
365                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
366             }
367             break;
368         case "eraAbbr":
369             // for FormatData
370             // create string array entry
371             if (currentCalendarType == null) {
372                 assert currentContainer instanceof IgnoredContainer;
373                 pushIgnoredContainer(qName);
374             } else {
375                 String key = currentCalendarType.keyElementName() + "Eras";
376                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
377             }
378             break;
379         case "eraNarrow":
380             // mainly used for the Japanese imperial calendar
381             if (currentCalendarType == null) {
382                 assert currentContainer instanceof IgnoredContainer;
383                 pushIgnoredContainer(qName);
384             } else {
385                 String key = currentCalendarType.keyElementName() + "narrow.Eras";
386                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
387             }
388             break;
389         case "era":
390             // for FormatData
391             // add to string array entry of eraAbbr element
392             if (currentCalendarType == null) {
393                 assert currentContainer instanceof IgnoredContainer;
394                 pushIgnoredContainer(qName);
395             } else {
396                 int index = Integer.parseInt(attributes.getValue("type"));
397                 index = currentCalendarType.normalizeEraIndex(index);
398                 if (index >= 0) {
399                     pushStringArrayElement(qName, attributes, index);
400                 } else {
401                     pushIgnoredContainer(qName);
402                 }
403                 if (currentContainer.getParent() == null) {
404                     throw new InternalError("currentContainer: null parent");
405                 }
406             }
407             break;
408         case "quarterContext":
409             {
410                 // for FormatData
411                 // need to keep stand-alone and format, to allow for inheritance in CLDR
412                 String type = attributes.getValue("type");
413                 if ("stand-alone".equals(type) || "format".equals(type)) {
414                     currentContext = type;
415                     pushKeyContainer(qName, attributes, type);
416                 } else {
417                     pushIgnoredContainer(qName);
418                 }
419             }
420             break;
421         case "quarterWidth":
422             {
423                 // for FormatData
424                 // keep info about the context type so we can sort out inheritance later
425                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
426                 currentWidth = attributes.getValue("type");
427                 switch (currentWidth) {
428                 case "wide":
429                     pushStringArrayEntry(qName, attributes, prefix + "QuarterNames/" + getContainerKey(), 4);
430                     break;
431                 case "abbreviated":
432                     pushStringArrayEntry(qName, attributes, prefix + "QuarterAbbreviations/" + getContainerKey(), 4);
433                     break;
434                 case "narrow":
435                     pushStringArrayEntry(qName, attributes, prefix + "QuarterNarrows/" + getContainerKey(), 4);
436                     break;
437                 default:
438                     pushIgnoredContainer(qName);
439                     break;
440                 }
441             }
442             break;
443         case "quarter":
444             // for FormatData
445             // add to string array entry of quarterWidth element
446             pushStringArrayElement(qName, attributes, Integer.parseInt(attributes.getValue("type")) - 1);
447             break;
448 
449         //
450         // Time zone names
451         //
452         case "timeZoneNames":
453             pushContainer(qName, attributes);
454             break;
455         case "hourFormat":
456             pushStringEntry(qName, attributes, "timezone.hourFormat");
457             break;
458         case "gmtFormat":
459             pushStringEntry(qName, attributes, "timezone.gmtFormat");
460             break;
461         case "gmtZeroFormat":
462             pushStringEntry(qName, attributes, "timezone.gmtZeroFormat");
463             break;
464         case "regionFormat":
465             {
466                 String type = attributes.getValue("type");
467                 pushStringEntry(qName, attributes, "timezone.regionFormat" +
468                     (type == null ? "" : "." + type));
469             }
470             break;
471         case "zone":
472             {
473                 String tzid = attributes.getValue("type"); // Olson tz id
474                 zonePrefix = CLDRConverter.TIMEZONE_ID_PREFIX;
475                 put(zonePrefix + tzid, new HashMap<String, String>());
476                 pushKeyContainer(qName, attributes, tzid);
477             }
478             break;
479         case "metazone":
480             {
481                 String zone = attributes.getValue("type"); // LDML meta zone id
482                 zonePrefix = CLDRConverter.METAZONE_ID_PREFIX;
483                 put(zonePrefix + zone, new HashMap<String, String>());
484                 pushKeyContainer(qName, attributes, zone);
485             }
486             break;
487         case "long":
488             zoneNameStyle = "long";
489             pushContainer(qName, attributes);
490             break;
491         case "short":
492             zoneNameStyle = "short";
493             pushContainer(qName, attributes);
494             break;
495         case "generic":  // generic name
496         case "standard": // standard time name
497         case "daylight": // daylight saving (summer) time name
498             pushStringEntry(qName, attributes, CLDRConverter.ZONE_NAME_PREFIX + qName + "." + zoneNameStyle);
499             break;
500         case "exemplarCity":
501             pushStringEntry(qName, attributes, CLDRConverter.EXEMPLAR_CITY_PREFIX);
502             break;
503 
504         //
505         // Number format information
506         //
507         case "decimalFormatLength":
508             String type = attributes.getValue("type");
509             if (null == type) {
510                 // format data for decimal number format
511                 pushStringEntry(qName, attributes,
512                     currentNumberingSystem + "NumberPatterns/decimal");
513                 currentStyle = type;
514             } else {
515                 switch (type) {
516                     case "short":
517                     case "long":
518                         // considering "short" and long for
519                         // compact number formatting patterns
520                         pushKeyContainer(qName, attributes, type);
521                         currentStyle = type;
522                         break;
523                     default:
524                         pushIgnoredContainer(qName);
525                         break;
526                 }
527             }
528             break;
529         case "decimalFormat":
530             if(currentStyle == null) {
531                 pushContainer(qName, attributes);
532             } else {
533                 switch (currentStyle) {
534                     case "short":
535                     case "long":
536                         pushStringListEntry(qName, attributes,
537                                 currentStyle+".CompactNumberPatterns");
538                         break;
539                     default:
540                         pushIgnoredContainer(qName);
541                         break;
542                 }
543             }
544             break;
545         case "currencyFormat":
546         case "percentFormat":
547             pushKeyContainer(qName, attributes, attributes.getValue("type"));
548             break;
549 
550         case "pattern":
551             String containerName = currentContainer.getqName();
552             switch (containerName) {
553                 case "currencyFormat":
554                 case "percentFormat":
555                 {
556                     // for FormatData
557                     // copy string for later assembly into NumberPatterns
558                     if (currentContainer instanceof KeyContainer) {
559                         String fStyle = ((KeyContainer)currentContainer).getKey();
560                         if (fStyle.equals("standard")) {
561                             pushStringEntry(qName, attributes,
562                                     currentNumberingSystem + "NumberPatterns/" + containerName.replaceFirst("Format", ""));
563                         } else if (fStyle.equals("accounting") && containerName.equals("currencyFormat")) {
564                             pushStringEntry(qName, attributes,
565                                     currentNumberingSystem + "NumberPatterns/accounting");
566                         } else {
567                             pushIgnoredContainer(qName);
568                         }
569                     } else {
570                         pushIgnoredContainer(qName);
571                     }
572                 }
573                 break;
574 
575                 case "decimalFormat":
576                     if (currentStyle == null) {
577                         pushContainer(qName, attributes);
578                     } else {
579                         switch (currentStyle) {
580                             case "short":
581                             case "long":
582                                 pushStringListElement(qName, attributes,
583                                     (int) Math.log10(Double.parseDouble(attributes.getValue("type"))),
584                                     attributes.getValue("count"));
585                                 break;
586                             default:
587                                 pushIgnoredContainer(qName);
588                                 break;
589                         }
590                     }
591                     break;
592                 default:
593                     pushContainer(qName, attributes);
594                     break;
595             }
596             break;
597         case "currencyFormats":
598         case "decimalFormats":
599         case "percentFormats":
600             {
601                 String script = attributes.getValue("numberSystem");
602                 if (script != null) {
603                     addNumberingScript(script);
604                     currentNumberingSystem = script + ".";
605                 }
606                 pushContainer(qName, attributes);
607             }
608             break;
609         case "currencyFormatLength":
610             if (attributes.getValue("type") == null) {
611                 // skipping type="short" data
612                 // for FormatData
613                 pushContainer(qName, attributes);
614             } else {
615                 pushIgnoredContainer(qName);
616             }
617             break;
618         case "defaultNumberingSystem":
619             // default numbering system if multiple numbering systems are used.
620             pushStringEntry(qName, attributes, "DefaultNumberingSystem");
621             break;
622         case "symbols":
623             // for FormatData
624             // look up numberingSystems
625             symbols: {
626                 String script = attributes.getValue("numberSystem");
627                 if (script == null) {
628                     // Has no script. Just ignore.
629                     pushIgnoredContainer(qName);
630                     break;
631                 }
632 
633                 // Use keys as <script>."NumberElements/<symbol>"
634                 currentNumberingSystem = script + ".";
635                 String digits = CLDRConverter.handlerNumbering.get(script);
636                 if (digits == null) {
637                     pushIgnoredContainer(qName);
638                     break;
639                 }
640 
641                 addNumberingScript(script);
642                 put(currentNumberingSystem + "NumberElements/zero", digits.substring(0, 1));
643                 pushContainer(qName, attributes);
644             }
645             break;
646         case "decimal":
647             // for FormatData
648             // copy string for later assembly into NumberElements
649             if (currentContainer.getqName().equals("symbols")) {
650                 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/decimal");
651             } else {
652                 pushIgnoredContainer(qName);
653             }
654             break;
655         case "group":
656             // for FormatData
657             // copy string for later assembly into NumberElements
658             if (currentContainer.getqName().equals("symbols")) {
659                 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/group");
660             } else {
661                 pushIgnoredContainer(qName);
662             }
663             break;
664         case "list":
665             // for FormatData
666             // copy string for later assembly into NumberElements
667             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/list");
668             break;
669         case "percentSign":
670             // for FormatData
671             // copy string for later assembly into NumberElements
672             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/percent");
673             break;
674         case "nativeZeroDigit":
675             // for FormatData
676             // copy string for later assembly into NumberElements
677             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/zero");
678             break;
679         case "patternDigit":
680             // for FormatData
681             // copy string for later assembly into NumberElements
682             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/pattern");
683             break;
684         case "plusSign":
685             // TODO: DecimalFormatSymbols doesn't support plusSign
686             pushIgnoredContainer(qName);
687             break;
688         case "minusSign":
689             // for FormatData
690             // copy string for later assembly into NumberElements
691             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/minus");
692             break;
693         case "exponential":
694             // for FormatData
695             // copy string for later assembly into NumberElements
696             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/exponential");
697             break;
698         case "perMille":
699             // for FormatData
700             // copy string for later assembly into NumberElements
701             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/permille");
702             break;
703         case "infinity":
704             // for FormatData
705             // copy string for later assembly into NumberElements
706             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/infinity");
707             break;
708         case "nan":
709             // for FormatData
710             // copy string for later assembly into NumberElements
711             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/nan");
712             break;
713         case "timeFormatLength":
714             {
715                 // for FormatData
716                 // copy string for later assembly into DateTimePatterns
717                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
718                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-time");
719             }
720             break;
721         case "dateFormatLength":
722             {
723                 // for FormatData
724                 // copy string for later assembly into DateTimePatterns
725                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
726                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-date");
727             }
728             break;
729         case "dateTimeFormatLength":
730             {
731                 // for FormatData
732                 // copy string for later assembly into DateTimePatterns
733                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
734                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-dateTime");
735             }
736             break;
737         case "localizedPatternChars":
738             {
739                 // for FormatData
740                 // copy string for later adaptation to JRE use
741                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
742                 pushStringEntry(qName, attributes, prefix + "DateTimePatternChars");
743             }
744             break;
745 
746         // "alias" for root
747         case "alias":
748             {
749                 if (id.equals("root") && !isIgnored(attributes)
750                         && ((currentContainer.getqName().equals("decimalFormatLength"))
751                         || (currentContainer.getqName().equals("currencyFormat"))
752                         || (currentContainer.getqName().equals("percentFormat"))
753                         || (currentCalendarType != null && !currentCalendarType.lname().startsWith("islamic-")))) { // ignore islamic variants
754                     pushAliasEntry(qName, attributes, attributes.getValue("path"));
755                 } else {
756                     pushIgnoredContainer(qName);
757                 }
758             }
759             break;
760 
761         default:
762             // treat anything else as a container
763             pushContainer(qName, attributes);
764             break;
765         }
766     }
767 
768     private static final String[] CONTEXTS = {"stand-alone", "format"};
769     private static final String[] WIDTHS = {"wide", "narrow", "abbreviated"};
770     private static final String[] LENGTHS = {"full", "long", "medium", "short"};
771 
populateWidthAlias(String type, Set<String> keys)772     private void populateWidthAlias(String type, Set<String> keys) {
773         for (String context : CONTEXTS) {
774             for (String width : WIDTHS) {
775                 String keyName = toJDKKey(type+"Width", context, width);
776                 if (keyName.length() > 0) {
777                     keys.add(keyName + "," + context + "," + width);
778                 }
779             }
780         }
781     }
782 
populateFormatLengthAlias(String type, Set<String> keys)783     private void populateFormatLengthAlias(String type, Set<String> keys) {
784         for (String length: LENGTHS) {
785             String keyName = toJDKKey(type+"FormatLength", currentContext, length);
786             if (keyName.length() > 0) {
787                 keys.add(keyName + "," + currentContext + "," + length);
788             }
789         }
790     }
791 
populateAliasKeys(String qName, String context, String width)792     private Set<String> populateAliasKeys(String qName, String context, String width) {
793         HashSet<String> ret = new HashSet<>();
794         String keyName = qName;
795 
796         switch (qName) {
797         case "monthWidth":
798         case "dayWidth":
799         case "quarterWidth":
800         case "dayPeriodWidth":
801         case "dateFormatLength":
802         case "timeFormatLength":
803         case "dateTimeFormatLength":
804         case "eraNames":
805         case "eraAbbr":
806         case "eraNarrow":
807             ret.add(toJDKKey(qName, context, width) + "," + context + "," + width);
808             break;
809         case "days":
810             populateWidthAlias("day", ret);
811             break;
812         case "months":
813             populateWidthAlias("month", ret);
814             break;
815         case "quarters":
816             populateWidthAlias("quarter", ret);
817             break;
818         case "dayPeriods":
819             populateWidthAlias("dayPeriod", ret);
820             break;
821         case "eras":
822             ret.add(toJDKKey("eraNames", context, width) + "," + context + "," + width);
823             ret.add(toJDKKey("eraAbbr", context, width) + "," + context + "," + width);
824             ret.add(toJDKKey("eraNarrow", context, width) + "," + context + "," + width);
825             break;
826         case "dateFormats":
827             populateFormatLengthAlias("date", ret);
828             break;
829         case "timeFormats":
830             populateFormatLengthAlias("time", ret);
831             break;
832         default:
833             break;
834         }
835         return ret;
836     }
837 
translateWidthAlias(String qName, String context, String width)838     private String translateWidthAlias(String qName, String context, String width) {
839         String keyName = qName;
840         String type = Character.toUpperCase(qName.charAt(0)) + qName.substring(1, qName.indexOf("Width"));
841 
842         switch (width) {
843         case "wide":
844             keyName = type + "Names/" + context;
845             break;
846         case "abbreviated":
847             keyName = type + "Abbreviations/" + context;
848             break;
849         case "narrow":
850             keyName = type + "Narrows/" + context;
851             break;
852         default:
853             assert false;
854         }
855 
856         return keyName;
857     }
858 
toJDKKey(String containerqName, String context, String type)859     private String toJDKKey(String containerqName, String context, String type) {
860         String keyName = containerqName;
861 
862         switch (containerqName) {
863         case "monthWidth":
864         case "dayWidth":
865         case "quarterWidth":
866             keyName = translateWidthAlias(keyName, context, type);
867             break;
868         case "dayPeriodWidth":
869             switch (type) {
870             case "wide":
871                 keyName = "AmPmMarkers/" + context;
872                 break;
873             case "narrow":
874                 keyName = "narrow.AmPmMarkers/" + context;
875                 break;
876             case "abbreviated":
877                 keyName = "abbreviated.AmPmMarkers/" + context;
878                 break;
879             }
880             break;
881         case "dateFormatLength":
882         case "timeFormatLength":
883         case "dateTimeFormatLength":
884             keyName = "DateTimePatterns/" +
885                 type + "-" +
886                 keyName.substring(0, keyName.indexOf("FormatLength"));
887             break;
888         case "eraNames":
889             keyName = "long.Eras";
890             break;
891         case "eraAbbr":
892             keyName = "Eras";
893             break;
894         case "eraNarrow":
895             keyName = "narrow.Eras";
896             break;
897         case "dateFormats":
898         case "timeFormats":
899         case "days":
900         case "months":
901         case "quarters":
902         case "dayPeriods":
903         case "eras":
904             break;
905         case "decimalFormatLength": // used for compact number formatting patterns
906             keyName = type + ".CompactNumberPatterns";
907             break;
908         case "currencyFormat":
909         case "percentFormat":
910             keyName = currentNumberingSystem +
911                     "NumberPatterns/" +
912                     (type.equals("standard") ? containerqName.replaceFirst("Format", "") : type);
913             break;
914         default:
915             keyName = "";
916             break;
917         }
918 
919         return keyName;
920     }
921 
getTarget(String path, String calType, String context, String width)922     private String getTarget(String path, String calType, String context, String width) {
923         // Target qName
924         int lastSlash = path.lastIndexOf('/');
925         String qName = path.substring(lastSlash+1);
926         int bracket = qName.indexOf('[');
927         if (bracket != -1) {
928             qName = qName.substring(0, bracket);
929         }
930 
931         // calType
932         String typeKey = "/calendar[@type='";
933         int start = path.indexOf(typeKey);
934         if (start != -1) {
935             calType = path.substring(start+typeKey.length(), path.indexOf("']", start));
936         }
937 
938         // context
939         typeKey = "Context[@type='";
940         start = path.indexOf(typeKey);
941         if (start != -1) {
942             context = (path.substring(start+typeKey.length(), path.indexOf("']", start)));
943         }
944 
945         // width
946         typeKey = "Width[@type='";
947         start = path.indexOf(typeKey);
948         if (start != -1) {
949             width = path.substring(start+typeKey.length(), path.indexOf("']", start));
950         }
951 
952         // used for compact number formatting patterns aliases
953         typeKey = "decimalFormatLength[@type='";
954         start = path.indexOf(typeKey);
955         if (start != -1) {
956             String style = path.substring(start + typeKey.length(), path.indexOf("']", start));
957             return toJDKKey(qName, "", style);
958         }
959 
960         // currencyFormat
961         typeKey = "currencyFormat[@type='";
962         start = path.indexOf(typeKey);
963         if (start != -1) {
964             String style = path.substring(start + typeKey.length(), path.indexOf("']", start));
965             return toJDKKey(qName, "", style);
966         }
967 
968         // percentFormat
969         typeKey = "percentFormat[@type='";
970         start = path.indexOf(typeKey);
971         if (start != -1) {
972             String style = path.substring(start + typeKey.length(), path.indexOf("']", start));
973             return toJDKKey(qName, "", style);
974         }
975 
976         return calType + "." + toJDKKey(qName, context, width);
977     }
978 
979     @Override
endElement(String uri, String localName, String qName)980     public void endElement(String uri, String localName, String qName) throws SAXException {
981         assert qName.equals(currentContainer.getqName()) : "current=" + currentContainer.getqName() + ", param=" + qName;
982         switch (qName) {
983         case "calendar":
984             assert !(currentContainer instanceof Entry);
985             currentCalendarType = null;
986             break;
987 
988         case "defaultNumberingSystem":
989             if (currentContainer instanceof StringEntry) {
990                 defaultNumberingSystem = (String) putIfEntry();
991             } else {
992                 defaultNumberingSystem = null;
993             }
994             break;
995 
996         case "timeZoneNames":
997             zonePrefix = null;
998             break;
999 
1000         case "generic":
1001         case "standard":
1002         case "daylight":
1003         case "exemplarCity":
1004             if (zonePrefix != null && (currentContainer instanceof Entry)) {
1005                 @SuppressWarnings("unchecked")
1006                 Map<String, String> valmap = (Map<String, String>) get(zonePrefix + getContainerKey());
1007                 Entry<?> entry = (Entry<?>) currentContainer;
1008                 if (qName.equals("exemplarCity")) {
1009                     put(CLDRConverter.EXEMPLAR_CITY_PREFIX + getContainerKey(), (String) entry.getValue());
1010                 } else {
1011                     valmap.put(entry.getKey(), (String) entry.getValue());
1012                 }
1013             }
1014             break;
1015 
1016         case "monthWidth":
1017         case "dayWidth":
1018         case "dayPeriodWidth":
1019         case "quarterWidth":
1020             currentWidth = "";
1021             putIfEntry();
1022             break;
1023 
1024         case "monthContext":
1025         case "dayContext":
1026         case "dayPeriodContext":
1027         case "quarterContext":
1028             currentContext = "";
1029             putIfEntry();
1030             break;
1031         case "decimalFormatLength":
1032             currentStyle = "";
1033             putIfEntry();
1034             break;
1035         case "currencyFormats":
1036         case "decimalFormats":
1037         case "percentFormats":
1038         case "symbols":
1039             currentNumberingSystem = "";
1040             putIfEntry();
1041             break;
1042         default:
1043             putIfEntry();
1044         }
1045         currentContainer = currentContainer.getParent();
1046     }
1047 
putIfEntry()1048     private Object putIfEntry() {
1049         if (currentContainer instanceof AliasEntry) {
1050             Entry<?> entry = (Entry<?>) currentContainer;
1051             String containerqName = entry.getParent().getqName();
1052             if (containerqName.equals("decimalFormatLength")) {
1053                 String srcKey = toJDKKey(containerqName, "", currentStyle);
1054                 String targetKey = getTarget(entry.getKey(), "", "", "");
1055                 CLDRConverter.aliases.put(srcKey, targetKey);
1056             } else if (containerqName.equals("currencyFormat") ||
1057                         containerqName.equals("percentFormat")) {
1058                 KeyContainer kc = (KeyContainer)entry.getParent();
1059                 CLDRConverter.aliases.put(
1060                         toJDKKey(containerqName, "", kc.getKey()),
1061                         getTarget(entry.getKey(), "", "", "")
1062                 );
1063             } else {
1064                 Set<String> keyNames = populateAliasKeys(containerqName, currentContext, currentWidth);
1065                 if (!keyNames.isEmpty()) {
1066                     for (String keyName : keyNames) {
1067                         String[] tmp = keyName.split(",", 3);
1068                         String calType = currentCalendarType.lname();
1069                         String src = calType+"."+tmp[0];
1070                         String target = getTarget(
1071                                     entry.getKey(),
1072                                     calType,
1073                                     tmp[1].length()>0 ? tmp[1] : currentContext,
1074                                     tmp[2].length()>0 ? tmp[2] : currentWidth);
1075                         if (target.substring(target.lastIndexOf('.')+1).equals(containerqName)) {
1076                             target = target.substring(0, target.indexOf('.'))+"."+tmp[0];
1077                         }
1078                         CLDRConverter.aliases.put(src.replaceFirst("^gregorian.", ""),
1079                                                   target.replaceFirst("^gregorian.", ""));
1080                     }
1081                 }
1082             }
1083         } else if (currentContainer instanceof Entry) {
1084             Entry<?> entry = (Entry<?>) currentContainer;
1085             Object value = entry.getValue();
1086             if (value != null) {
1087                 String key = entry.getKey();
1088                 // Tweak for MonthNames for the root locale, Needed for
1089                 // SimpleDateFormat.format()/parse() roundtrip.
1090                 if (id.equals("root") && key.startsWith("MonthNames")) {
1091                     value = new DateFormatSymbols(Locale.US).getShortMonths();
1092                 }
1093                 return put(entry.getKey(), value);
1094             }
1095         }
1096         return null;
1097     }
1098 
convertOldKeyName(String key)1099     public String convertOldKeyName(String key) {
1100         // Explicitly obtained from "alias" attribute in each "key" element.
1101         switch (key) {
1102             case "calendar":
1103                 return "ca";
1104             case "currency":
1105                 return "cu";
1106             case "collation":
1107                 return "co";
1108             case "numbers":
1109                 return "nu";
1110             case "timezone":
1111                 return "tz";
1112             default:
1113                 return key;
1114         }
1115     }
1116 
addNumberingScript(String script)1117     private void addNumberingScript(String script) {
1118         @SuppressWarnings("unchecked")
1119         List<String> numberingScripts = (List<String>) get("numberingScripts");
1120         if (numberingScripts == null) {
1121             numberingScripts = new ArrayList<>();
1122             put("numberingScripts", numberingScripts);
1123         }
1124         if (!numberingScripts.contains(script)) {
1125             numberingScripts.add(script);
1126         }
1127     }
1128 }
1129