1 /*
2  * Copyright (c) 2012, 2020, 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             {
306                 String type = attributes.getValue("type");
307                 if ("stand-alone".equals(type) || "format".equals(type)) {
308                     currentContext = type;
309                     pushKeyContainer(qName, attributes, type);
310                 } else {
311                     pushIgnoredContainer(qName);
312                 }
313             }
314             break;
315         case "dayPeriodWidth":
316             // for FormatData
317             // create string array entry for am/pm.
318             currentWidth = attributes.getValue("type");
319             switch (currentWidth) {
320             case "wide":
321                 pushStringArrayEntry(qName, attributes, "AmPmMarkers/" + getContainerKey(), 12);
322                 break;
323             case "narrow":
324                 pushStringArrayEntry(qName, attributes, "narrow.AmPmMarkers/" + getContainerKey(), 12);
325                 break;
326             case "abbreviated":
327                 pushStringArrayEntry(qName, attributes, "abbreviated.AmPmMarkers/" + getContainerKey(), 12);
328                 break;
329             default:
330                 pushIgnoredContainer(qName);
331                 break;
332             }
333             break;
334         case "dayPeriod":
335             // for FormatData
336             // add to string array entry of AmPmMarkers element
337             if (attributes.getValue("alt") == null) {
338                 switch (attributes.getValue("type")) {
339                 case "am":
340                     pushStringArrayElement(qName, attributes, 0);
341                     break;
342                 case "pm":
343                     pushStringArrayElement(qName, attributes, 1);
344                     break;
345                 case "midnight":
346                     pushStringArrayElement(qName, attributes, 2);
347                     break;
348                 case "noon":
349                     pushStringArrayElement(qName, attributes, 3);
350                     break;
351                 case "morning1":
352                     pushStringArrayElement(qName, attributes, 4);
353                     break;
354                 case "morning2":
355                     pushStringArrayElement(qName, attributes, 5);
356                     break;
357                 case "afternoon1":
358                     pushStringArrayElement(qName, attributes, 6);
359                     break;
360                 case "afternoon2":
361                     pushStringArrayElement(qName, attributes, 7);
362                     break;
363                 case "evening1":
364                     pushStringArrayElement(qName, attributes, 8);
365                     break;
366                 case "evening2":
367                     pushStringArrayElement(qName, attributes, 9);
368                     break;
369                 case "night1":
370                     pushStringArrayElement(qName, attributes, 10);
371                     break;
372                 case "night2":
373                     pushStringArrayElement(qName, attributes, 11);
374                     break;
375                 default:
376                     pushIgnoredContainer(qName);
377                     break;
378                 }
379             } else {
380                 // discard alt values
381                 pushIgnoredContainer(qName);
382             }
383             break;
384         case "eraNames":
385             // CLDR era names are inconsistent in terms of their lengths. For example,
386             // the full names of Japanese imperial eras are eraAbbr, while the full names
387             // of the Julian eras are eraNames.
388             if (currentCalendarType == null) {
389                 assert currentContainer instanceof IgnoredContainer;
390                 pushIgnoredContainer(qName);
391             } else {
392                 String key = currentCalendarType.keyElementName() + "long.Eras"; // for now
393                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
394             }
395             break;
396         case "eraAbbr":
397             // for FormatData
398             // create string array entry
399             if (currentCalendarType == null) {
400                 assert currentContainer instanceof IgnoredContainer;
401                 pushIgnoredContainer(qName);
402             } else {
403                 String key = currentCalendarType.keyElementName() + "Eras";
404                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
405             }
406             break;
407         case "eraNarrow":
408             // mainly used for the Japanese imperial calendar
409             if (currentCalendarType == null) {
410                 assert currentContainer instanceof IgnoredContainer;
411                 pushIgnoredContainer(qName);
412             } else {
413                 String key = currentCalendarType.keyElementName() + "narrow.Eras";
414                 pushStringArrayEntry(qName, attributes, key, currentCalendarType.getEraLength(qName));
415             }
416             break;
417         case "era":
418             // for FormatData
419             // add to string array entry of eraAbbr element
420             if (currentCalendarType == null) {
421                 assert currentContainer instanceof IgnoredContainer;
422                 pushIgnoredContainer(qName);
423             } else {
424                 int index = Integer.parseInt(attributes.getValue("type"));
425                 index = currentCalendarType.normalizeEraIndex(index);
426                 if (index >= 0) {
427                     pushStringArrayElement(qName, attributes, index);
428                 } else {
429                     pushIgnoredContainer(qName);
430                 }
431                 if (currentContainer.getParent() == null) {
432                     throw new InternalError("currentContainer: null parent");
433                 }
434             }
435             break;
436         case "quarterContext":
437             {
438                 // for FormatData
439                 // need to keep stand-alone and format, to allow for inheritance in CLDR
440                 String type = attributes.getValue("type");
441                 if ("stand-alone".equals(type) || "format".equals(type)) {
442                     currentContext = type;
443                     pushKeyContainer(qName, attributes, type);
444                 } else {
445                     pushIgnoredContainer(qName);
446                 }
447             }
448             break;
449         case "quarterWidth":
450             {
451                 // for FormatData
452                 // keep info about the context type so we can sort out inheritance later
453                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
454                 currentWidth = attributes.getValue("type");
455                 switch (currentWidth) {
456                 case "wide":
457                     pushStringArrayEntry(qName, attributes, prefix + "QuarterNames/" + getContainerKey(), 4);
458                     break;
459                 case "abbreviated":
460                     pushStringArrayEntry(qName, attributes, prefix + "QuarterAbbreviations/" + getContainerKey(), 4);
461                     break;
462                 case "narrow":
463                     pushStringArrayEntry(qName, attributes, prefix + "QuarterNarrows/" + getContainerKey(), 4);
464                     break;
465                 default:
466                     pushIgnoredContainer(qName);
467                     break;
468                 }
469             }
470             break;
471         case "quarter":
472             // for FormatData
473             // add to string array entry of quarterWidth element
474             pushStringArrayElement(qName, attributes, Integer.parseInt(attributes.getValue("type")) - 1);
475             break;
476 
477         //
478         // Time zone names
479         //
480         case "timeZoneNames":
481             pushContainer(qName, attributes);
482             break;
483         case "hourFormat":
484             pushStringEntry(qName, attributes, "timezone.hourFormat");
485             break;
486         case "gmtFormat":
487             pushStringEntry(qName, attributes, "timezone.gmtFormat");
488             break;
489         case "gmtZeroFormat":
490             pushStringEntry(qName, attributes, "timezone.gmtZeroFormat");
491             break;
492         case "regionFormat":
493             {
494                 String type = attributes.getValue("type");
495                 pushStringEntry(qName, attributes, "timezone.regionFormat" +
496                     (type == null ? "" : "." + type));
497             }
498             break;
499         case "zone":
500             {
501                 String tzid = attributes.getValue("type"); // Olson tz id
502                 zonePrefix = CLDRConverter.TIMEZONE_ID_PREFIX;
503                 put(zonePrefix + tzid, new HashMap<String, String>());
504                 pushKeyContainer(qName, attributes, tzid);
505             }
506             break;
507         case "metazone":
508             {
509                 String zone = attributes.getValue("type"); // LDML meta zone id
510                 zonePrefix = CLDRConverter.METAZONE_ID_PREFIX;
511                 put(zonePrefix + zone, new HashMap<String, String>());
512                 pushKeyContainer(qName, attributes, zone);
513             }
514             break;
515         case "long":
516             zoneNameStyle = "long";
517             pushContainer(qName, attributes);
518             break;
519         case "short":
520             zoneNameStyle = "short";
521             pushContainer(qName, attributes);
522             break;
523         case "generic":  // generic name
524         case "standard": // standard time name
525         case "daylight": // daylight saving (summer) time name
526             pushStringEntry(qName, attributes, CLDRConverter.ZONE_NAME_PREFIX + qName + "." + zoneNameStyle);
527             break;
528         case "exemplarCity":
529             pushStringEntry(qName, attributes, CLDRConverter.EXEMPLAR_CITY_PREFIX);
530             break;
531 
532         //
533         // Number format information
534         //
535         case "decimalFormatLength":
536             String type = attributes.getValue("type");
537             if (null == type) {
538                 // format data for decimal number format
539                 pushStringEntry(qName, attributes,
540                     currentNumberingSystem + "NumberPatterns/decimal");
541                 currentStyle = type;
542             } else {
543                 switch (type) {
544                     case "short":
545                     case "long":
546                         // considering "short" and long for
547                         // compact number formatting patterns
548                         pushKeyContainer(qName, attributes, type);
549                         currentStyle = type;
550                         break;
551                     default:
552                         pushIgnoredContainer(qName);
553                         break;
554                 }
555             }
556             break;
557         case "decimalFormat":
558             if(currentStyle == null) {
559                 pushContainer(qName, attributes);
560             } else {
561                 switch (currentStyle) {
562                     case "short":
563                     case "long":
564                         pushStringListEntry(qName, attributes,
565                                 currentStyle+".CompactNumberPatterns");
566                         break;
567                     default:
568                         pushIgnoredContainer(qName);
569                         break;
570                 }
571             }
572             break;
573         case "currencyFormat":
574         case "percentFormat":
575             pushKeyContainer(qName, attributes, attributes.getValue("type"));
576             break;
577 
578         case "pattern":
579             String containerName = currentContainer.getqName();
580             switch (containerName) {
581                 case "currencyFormat":
582                 case "percentFormat":
583                 {
584                     // for FormatData
585                     // copy string for later assembly into NumberPatterns
586                     if (currentContainer instanceof KeyContainer) {
587                         String fStyle = ((KeyContainer)currentContainer).getKey();
588                         if (fStyle.equals("standard")) {
589                             pushStringEntry(qName, attributes,
590                                     currentNumberingSystem + "NumberPatterns/" + containerName.replaceFirst("Format", ""));
591                         } else if (fStyle.equals("accounting") && containerName.equals("currencyFormat")) {
592                             pushStringEntry(qName, attributes,
593                                     currentNumberingSystem + "NumberPatterns/accounting");
594                         } else {
595                             pushIgnoredContainer(qName);
596                         }
597                     } else {
598                         pushIgnoredContainer(qName);
599                     }
600                 }
601                 break;
602 
603                 case "decimalFormat":
604                     if (currentStyle == null) {
605                         pushContainer(qName, attributes);
606                     } else {
607                         switch (currentStyle) {
608                             case "short":
609                             case "long":
610                                 pushStringListElement(qName, attributes,
611                                     (int) Math.log10(Double.parseDouble(attributes.getValue("type"))),
612                                     attributes.getValue("count"));
613                                 break;
614                             default:
615                                 pushIgnoredContainer(qName);
616                                 break;
617                         }
618                     }
619                     break;
620                 default:
621                     pushContainer(qName, attributes);
622                     break;
623             }
624             break;
625         case "currencyFormats":
626         case "decimalFormats":
627         case "percentFormats":
628             {
629                 String script = attributes.getValue("numberSystem");
630                 if (script != null) {
631                     addNumberingScript(script);
632                     currentNumberingSystem = script + ".";
633                 }
634                 pushContainer(qName, attributes);
635             }
636             break;
637         case "currencyFormatLength":
638             if (attributes.getValue("type") == null) {
639                 // skipping type="short" data
640                 // for FormatData
641                 pushContainer(qName, attributes);
642             } else {
643                 pushIgnoredContainer(qName);
644             }
645             break;
646         case "defaultNumberingSystem":
647             // default numbering system if multiple numbering systems are used.
648             pushStringEntry(qName, attributes, "DefaultNumberingSystem");
649             break;
650         case "symbols":
651             // for FormatData
652             // look up numberingSystems
653             symbols: {
654                 String script = attributes.getValue("numberSystem");
655                 if (script == null) {
656                     // Has no script. Just ignore.
657                     pushIgnoredContainer(qName);
658                     break;
659                 }
660 
661                 // Use keys as <script>."NumberElements/<symbol>"
662                 currentNumberingSystem = script + ".";
663                 String digits = CLDRConverter.handlerNumbering.get(script);
664                 if (digits == null) {
665                     pushIgnoredContainer(qName);
666                     break;
667                 }
668 
669                 addNumberingScript(script);
670                 put(currentNumberingSystem + "NumberElements/zero", digits.substring(0, 1));
671                 pushContainer(qName, attributes);
672             }
673             break;
674         case "decimal":
675         case "group":
676         case "currencyDecimal":
677         case "currencyGroup":
678             // for FormatData
679             // copy string for later assembly into NumberElements
680             if (currentContainer.getqName().equals("symbols")) {
681                 pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/" + qName);
682             } else {
683                 pushIgnoredContainer(qName);
684             }
685             break;
686         case "list":
687             // for FormatData
688             // copy string for later assembly into NumberElements
689             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/list");
690             break;
691         case "percentSign":
692             // for FormatData
693             // copy string for later assembly into NumberElements
694             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/percent");
695             break;
696         case "nativeZeroDigit":
697             // for FormatData
698             // copy string for later assembly into NumberElements
699             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/zero");
700             break;
701         case "patternDigit":
702             // for FormatData
703             // copy string for later assembly into NumberElements
704             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/pattern");
705             break;
706         case "plusSign":
707             // TODO: DecimalFormatSymbols doesn't support plusSign
708             pushIgnoredContainer(qName);
709             break;
710         case "minusSign":
711             // for FormatData
712             // copy string for later assembly into NumberElements
713             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/minus");
714             break;
715         case "exponential":
716             // for FormatData
717             // copy string for later assembly into NumberElements
718             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/exponential");
719             break;
720         case "perMille":
721             // for FormatData
722             // copy string for later assembly into NumberElements
723             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/permille");
724             break;
725         case "infinity":
726             // for FormatData
727             // copy string for later assembly into NumberElements
728             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/infinity");
729             break;
730         case "nan":
731             // for FormatData
732             // copy string for later assembly into NumberElements
733             pushStringEntry(qName, attributes, currentNumberingSystem + "NumberElements/nan");
734             break;
735         case "timeFormatLength":
736             {
737                 // for FormatData
738                 // copy string for later assembly into DateTimePatterns
739                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
740                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-time");
741             }
742             break;
743         case "dateFormatLength":
744             {
745                 // for FormatData
746                 // copy string for later assembly into DateTimePatterns
747                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
748                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-date");
749             }
750             break;
751         case "dateTimeFormatLength":
752             {
753                 // for FormatData
754                 // copy string for later assembly into DateTimePatterns
755                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
756                 pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-dateTime");
757             }
758             break;
759         case "localizedPatternChars":
760             {
761                 // for FormatData
762                 // copy string for later adaptation to JRE use
763                 String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName();
764                 pushStringEntry(qName, attributes, prefix + "DateTimePatternChars");
765             }
766             break;
767 
768         // "alias" for root
769         case "alias":
770             {
771                 if (id.equals("root") && !isIgnored(attributes)
772                         && ((currentContainer.getqName().equals("decimalFormatLength"))
773                         || (currentContainer.getqName().equals("currencyFormat"))
774                         || (currentContainer.getqName().equals("percentFormat"))
775                         || (currentCalendarType != null && !currentCalendarType.lname().startsWith("islamic-")))) { // ignore islamic variants
776                     pushAliasEntry(qName, attributes, attributes.getValue("path"));
777                 } else {
778                     pushIgnoredContainer(qName);
779                 }
780             }
781             break;
782 
783         default:
784             // treat anything else as a container
785             pushContainer(qName, attributes);
786             break;
787         }
788     }
789 
790     private static final String[] CONTEXTS = {"stand-alone", "format"};
791     private static final String[] WIDTHS = {"wide", "narrow", "abbreviated"};
792     private static final String[] LENGTHS = {"full", "long", "medium", "short"};
793 
populateWidthAlias(String type, Set<String> keys)794     private void populateWidthAlias(String type, Set<String> keys) {
795         for (String context : CONTEXTS) {
796             for (String width : WIDTHS) {
797                 String keyName = toJDKKey(type+"Width", context, width);
798                 if (keyName.length() > 0) {
799                     keys.add(keyName + "," + context + "," + width);
800                 }
801             }
802         }
803     }
804 
populateFormatLengthAlias(String type, Set<String> keys)805     private void populateFormatLengthAlias(String type, Set<String> keys) {
806         for (String length: LENGTHS) {
807             String keyName = toJDKKey(type+"FormatLength", currentContext, length);
808             if (keyName.length() > 0) {
809                 keys.add(keyName + "," + currentContext + "," + length);
810             }
811         }
812     }
813 
populateAliasKeys(String qName, String context, String width)814     private Set<String> populateAliasKeys(String qName, String context, String width) {
815         HashSet<String> ret = new HashSet<>();
816         String keyName = qName;
817 
818         switch (qName) {
819         case "monthWidth":
820         case "dayWidth":
821         case "quarterWidth":
822         case "dayPeriodWidth":
823         case "dateFormatLength":
824         case "timeFormatLength":
825         case "dateTimeFormatLength":
826         case "eraNames":
827         case "eraAbbr":
828         case "eraNarrow":
829             ret.add(toJDKKey(qName, context, width) + "," + context + "," + width);
830             break;
831         case "days":
832             populateWidthAlias("day", ret);
833             break;
834         case "months":
835             populateWidthAlias("month", ret);
836             break;
837         case "quarters":
838             populateWidthAlias("quarter", ret);
839             break;
840         case "dayPeriods":
841             populateWidthAlias("dayPeriod", ret);
842             break;
843         case "eras":
844             ret.add(toJDKKey("eraNames", context, width) + "," + context + "," + width);
845             ret.add(toJDKKey("eraAbbr", context, width) + "," + context + "," + width);
846             ret.add(toJDKKey("eraNarrow", context, width) + "," + context + "," + width);
847             break;
848         case "dateFormats":
849             populateFormatLengthAlias("date", ret);
850             break;
851         case "timeFormats":
852             populateFormatLengthAlias("time", ret);
853             break;
854         default:
855             break;
856         }
857         return ret;
858     }
859 
translateWidthAlias(String qName, String context, String width)860     private String translateWidthAlias(String qName, String context, String width) {
861         String keyName = qName;
862         String type = Character.toUpperCase(qName.charAt(0)) + qName.substring(1, qName.indexOf("Width"));
863 
864         switch (width) {
865         case "wide":
866             keyName = type + "Names/" + context;
867             break;
868         case "abbreviated":
869             keyName = type + "Abbreviations/" + context;
870             break;
871         case "narrow":
872             keyName = type + "Narrows/" + context;
873             break;
874         default:
875             assert false;
876         }
877 
878         return keyName;
879     }
880 
toJDKKey(String containerqName, String context, String type)881     private String toJDKKey(String containerqName, String context, String type) {
882         String keyName = containerqName;
883 
884         switch (containerqName) {
885         case "monthWidth":
886         case "dayWidth":
887         case "quarterWidth":
888             keyName = translateWidthAlias(keyName, context, type);
889             break;
890         case "dayPeriodWidth":
891             switch (type) {
892             case "wide":
893                 keyName = "AmPmMarkers/" + context;
894                 break;
895             case "narrow":
896                 keyName = "narrow.AmPmMarkers/" + context;
897                 break;
898             case "abbreviated":
899                 keyName = "abbreviated.AmPmMarkers/" + context;
900                 break;
901             }
902             break;
903         case "dateFormatLength":
904         case "timeFormatLength":
905         case "dateTimeFormatLength":
906             keyName = "DateTimePatterns/" +
907                 type + "-" +
908                 keyName.substring(0, keyName.indexOf("FormatLength"));
909             break;
910         case "eraNames":
911             keyName = "long.Eras";
912             break;
913         case "eraAbbr":
914             keyName = "Eras";
915             break;
916         case "eraNarrow":
917             keyName = "narrow.Eras";
918             break;
919         case "dateFormats":
920         case "timeFormats":
921         case "days":
922         case "months":
923         case "quarters":
924         case "dayPeriods":
925         case "eras":
926             break;
927         case "decimalFormatLength": // used for compact number formatting patterns
928             keyName = type + ".CompactNumberPatterns";
929             break;
930         case "currencyFormat":
931         case "percentFormat":
932             keyName = currentNumberingSystem +
933                     "NumberPatterns/" +
934                     (type.equals("standard") ? containerqName.replaceFirst("Format", "") : type);
935             break;
936         default:
937             keyName = "";
938             break;
939         }
940 
941         return keyName;
942     }
943 
getTarget(String path, String calType, String context, String width)944     private String getTarget(String path, String calType, String context, String width) {
945         // Target qName
946         int lastSlash = path.lastIndexOf('/');
947         String qName = path.substring(lastSlash+1);
948         int bracket = qName.indexOf('[');
949         if (bracket != -1) {
950             qName = qName.substring(0, bracket);
951         }
952 
953         // calType
954         String typeKey = "/calendar[@type='";
955         int start = path.indexOf(typeKey);
956         if (start != -1) {
957             calType = path.substring(start+typeKey.length(), path.indexOf("']", start));
958         }
959 
960         // context
961         typeKey = "Context[@type='";
962         start = path.indexOf(typeKey);
963         if (start != -1) {
964             context = (path.substring(start+typeKey.length(), path.indexOf("']", start)));
965         }
966 
967         // width
968         typeKey = "Width[@type='";
969         start = path.indexOf(typeKey);
970         if (start != -1) {
971             width = path.substring(start+typeKey.length(), path.indexOf("']", start));
972         }
973 
974         // used for compact number formatting patterns aliases
975         typeKey = "decimalFormatLength[@type='";
976         start = path.indexOf(typeKey);
977         if (start != -1) {
978             String style = path.substring(start + typeKey.length(), path.indexOf("']", start));
979             return toJDKKey(qName, "", style);
980         }
981 
982         // currencyFormat
983         typeKey = "currencyFormat[@type='";
984         start = path.indexOf(typeKey);
985         if (start != -1) {
986             String style = path.substring(start + typeKey.length(), path.indexOf("']", start));
987             return toJDKKey(qName, "", style);
988         }
989 
990         // percentFormat
991         typeKey = "percentFormat[@type='";
992         start = path.indexOf(typeKey);
993         if (start != -1) {
994             String style = path.substring(start + typeKey.length(), path.indexOf("']", start));
995             return toJDKKey(qName, "", style);
996         }
997 
998         return calType + "." + toJDKKey(qName, context, width);
999     }
1000 
1001     @Override
1002     @SuppressWarnings("fallthrough")
endElement(String uri, String localName, String qName)1003     public void endElement(String uri, String localName, String qName) throws SAXException {
1004         assert qName.equals(currentContainer.getqName()) : "current=" + currentContainer.getqName() + ", param=" + qName;
1005         switch (qName) {
1006         case "calendar":
1007             assert !(currentContainer instanceof Entry);
1008             currentCalendarType = null;
1009             break;
1010 
1011         case "defaultNumberingSystem":
1012             if (currentContainer instanceof StringEntry) {
1013                 defaultNumberingSystem = (String) putIfEntry();
1014             } else {
1015                 defaultNumberingSystem = null;
1016             }
1017             break;
1018 
1019         case "timeZoneNames":
1020             zonePrefix = null;
1021             break;
1022 
1023         case "generic":
1024         case "standard":
1025         case "daylight":
1026         case "exemplarCity":
1027             if (zonePrefix != null && (currentContainer instanceof Entry)) {
1028                 @SuppressWarnings("unchecked")
1029                 Map<String, String> valmap = (Map<String, String>) get(zonePrefix + getContainerKey());
1030                 Entry<?> entry = (Entry<?>) currentContainer;
1031                 if (qName.equals("exemplarCity")) {
1032                     put(CLDRConverter.EXEMPLAR_CITY_PREFIX + getContainerKey(), (String) entry.getValue());
1033                 } else {
1034                     valmap.put(entry.getKey(), (String) entry.getValue());
1035                 }
1036             }
1037             break;
1038 
1039         case "monthWidth":
1040         case "dayWidth":
1041         case "dayPeriodWidth":
1042         case "quarterWidth":
1043             currentWidth = "";
1044             putIfEntry();
1045             break;
1046 
1047         case "monthContext":
1048         case "dayContext":
1049         case "dayPeriodContext":
1050         case "quarterContext":
1051             currentContext = "";
1052             putIfEntry();
1053             break;
1054         case "decimalFormatLength":
1055             currentStyle = "";
1056             putIfEntry();
1057             break;
1058         case "currencyFormats":
1059         case "decimalFormats":
1060         case "percentFormats":
1061         case "symbols":
1062             currentNumberingSystem = "";
1063             putIfEntry();
1064             break;
1065         default:
1066             putIfEntry();
1067         }
1068         currentContainer = currentContainer.getParent();
1069     }
1070 
putIfEntry()1071     private Object putIfEntry() {
1072         if (currentContainer instanceof AliasEntry) {
1073             Entry<?> entry = (Entry<?>) currentContainer;
1074             String containerqName = entry.getParent().getqName();
1075             if (containerqName.equals("decimalFormatLength")) {
1076                 String srcKey = toJDKKey(containerqName, "", currentStyle);
1077                 String targetKey = getTarget(entry.getKey(), "", "", "");
1078                 CLDRConverter.aliases.put(srcKey, targetKey);
1079             } else if (containerqName.equals("currencyFormat") ||
1080                         containerqName.equals("percentFormat")) {
1081                 KeyContainer kc = (KeyContainer)entry.getParent();
1082                 CLDRConverter.aliases.put(
1083                         toJDKKey(containerqName, "", kc.getKey()),
1084                         getTarget(entry.getKey(), "", "", "")
1085                 );
1086             } else {
1087                 Set<String> keyNames = populateAliasKeys(containerqName, currentContext, currentWidth);
1088                 if (!keyNames.isEmpty()) {
1089                     for (String keyName : keyNames) {
1090                         String[] tmp = keyName.split(",", 3);
1091                         String calType = currentCalendarType.lname();
1092                         String src = calType+"."+tmp[0];
1093                         String target = getTarget(
1094                                     entry.getKey(),
1095                                     calType,
1096                                     tmp[1].length()>0 ? tmp[1] : currentContext,
1097                                     tmp[2].length()>0 ? tmp[2] : currentWidth);
1098                         if (target.substring(target.lastIndexOf('.')+1).equals(containerqName)) {
1099                             target = target.substring(0, target.indexOf('.'))+"."+tmp[0];
1100                         }
1101                         CLDRConverter.aliases.put(src.replaceFirst("^gregorian.", ""),
1102                                                   target.replaceFirst("^gregorian.", ""));
1103                     }
1104                 }
1105             }
1106         } else if (currentContainer instanceof Entry) {
1107             Entry<?> entry = (Entry<?>) currentContainer;
1108             Object value = entry.getValue();
1109             if (value != null) {
1110                 String key = entry.getKey();
1111                 // Tweak for MonthNames for the root locale, Needed for
1112                 // SimpleDateFormat.format()/parse() roundtrip.
1113                 if (id.equals("root") && key.startsWith("MonthNames")) {
1114                     value = new DateFormatSymbols(Locale.US).getShortMonths();
1115                 }
1116                 return put(entry.getKey(), value);
1117             }
1118         }
1119         return null;
1120     }
1121 
convertOldKeyName(String key)1122     public String convertOldKeyName(String key) {
1123         // Explicitly obtained from "alias" attribute in each "key" element.
1124         switch (key) {
1125             case "calendar":
1126                 return "ca";
1127             case "currency":
1128                 return "cu";
1129             case "collation":
1130                 return "co";
1131             case "numbers":
1132                 return "nu";
1133             case "timezone":
1134                 return "tz";
1135             default:
1136                 return key;
1137         }
1138     }
1139 
addNumberingScript(String script)1140     private void addNumberingScript(String script) {
1141         @SuppressWarnings("unchecked")
1142         List<String> numberingScripts = (List<String>) get("numberingScripts");
1143         if (numberingScripts == null) {
1144             numberingScripts = new ArrayList<>();
1145             put("numberingScripts", numberingScripts);
1146         }
1147         if (!numberingScripts.contains(script)) {
1148             numberingScripts.add(script);
1149         }
1150     }
1151 }
1152