1/*
2 * Copyright (c) 2012, 2017, 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
26package build.tools.cldrconverter;
27
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.EnumSet;
31import java.util.HashMap;
32import java.util.Iterator;
33import java.util.List;
34import java.util.Map;
35import java.util.Objects;
36
37class Bundle {
38    static enum Type {
39        LOCALENAMES, CURRENCYNAMES, TIMEZONENAMES, CALENDARDATA, FORMATDATA;
40
41        static EnumSet<Type> ALL_TYPES = EnumSet.of(LOCALENAMES,
42                                                    CURRENCYNAMES,
43                                                    TIMEZONENAMES,
44                                                    CALENDARDATA,
45                                                    FORMATDATA);
46    }
47
48    private final static Map<String, Bundle> bundles = new HashMap<>();
49
50    private final static String[] NUMBER_PATTERN_KEYS = {
51        "NumberPatterns/decimal",
52        "NumberPatterns/currency",
53        "NumberPatterns/percent"
54    };
55
56    private final static String[] NUMBER_ELEMENT_KEYS = {
57        "NumberElements/decimal",
58        "NumberElements/group",
59        "NumberElements/list",
60        "NumberElements/percent",
61        "NumberElements/zero",
62        "NumberElements/pattern",
63        "NumberElements/minus",
64        "NumberElements/exponential",
65        "NumberElements/permille",
66        "NumberElements/infinity",
67        "NumberElements/nan"
68    };
69
70    private final static String[] TIME_PATTERN_KEYS = {
71        "DateTimePatterns/full-time",
72        "DateTimePatterns/long-time",
73        "DateTimePatterns/medium-time",
74        "DateTimePatterns/short-time",
75    };
76
77    private final static String[] DATE_PATTERN_KEYS = {
78        "DateTimePatterns/full-date",
79        "DateTimePatterns/long-date",
80        "DateTimePatterns/medium-date",
81        "DateTimePatterns/short-date",
82    };
83
84    private final static String[] DATETIME_PATTERN_KEYS = {
85        "DateTimePatterns/full-dateTime",
86        "DateTimePatterns/long-dateTime",
87        "DateTimePatterns/medium-dateTime",
88        "DateTimePatterns/short-dateTime",
89    };
90
91    private final static String[] ERA_KEYS = {
92        "long.Eras",
93        "Eras",
94        "narrow.Eras"
95    };
96
97    // Keys for individual time zone names
98    private final static String TZ_GEN_LONG_KEY = "timezone.displayname.generic.long";
99    private final static String TZ_GEN_SHORT_KEY = "timezone.displayname.generic.short";
100    private final static String TZ_STD_LONG_KEY = "timezone.displayname.standard.long";
101    private final static String TZ_STD_SHORT_KEY = "timezone.displayname.standard.short";
102    private final static String TZ_DST_LONG_KEY = "timezone.displayname.daylight.long";
103    private final static String TZ_DST_SHORT_KEY = "timezone.displayname.daylight.short";
104    private final static String[] ZONE_NAME_KEYS = {
105        TZ_STD_LONG_KEY,
106        TZ_STD_SHORT_KEY,
107        TZ_DST_LONG_KEY,
108        TZ_DST_SHORT_KEY,
109        TZ_GEN_LONG_KEY,
110        TZ_GEN_SHORT_KEY
111    };
112
113    private final String id;
114    private final String cldrPath;
115    private final EnumSet<Type> bundleTypes;
116    private final String currencies;
117    private Map<String, Object> targetMap;
118
119    static Bundle getBundle(String id) {
120        return bundles.get(id);
121    }
122
123    @SuppressWarnings("ConvertToStringSwitch")
124    Bundle(String id, String cldrPath, String bundles, String currencies) {
125        this.id = id;
126        this.cldrPath = cldrPath;
127        if ("localenames".equals(bundles)) {
128            bundleTypes = EnumSet.of(Type.LOCALENAMES);
129        } else if ("currencynames".equals(bundles)) {
130            bundleTypes = EnumSet.of(Type.CURRENCYNAMES);
131        } else {
132            bundleTypes = Type.ALL_TYPES;
133        }
134        if (currencies == null) {
135            currencies = "local";
136        }
137        this.currencies = currencies;
138        addBundle();
139    }
140
141    private void addBundle() {
142        Bundle.bundles.put(id, this);
143    }
144
145    String getID() {
146        return id;
147    }
148
149    String getJavaID() {
150        // Tweak ISO compatibility for bundle generation
151        return id.replaceFirst("^he", "iw")
152            .replaceFirst("^id", "in")
153            .replaceFirst("^yi", "ji");
154    }
155
156    boolean isRoot() {
157        return "root".equals(id);
158    }
159
160    String getCLDRPath() {
161        return cldrPath;
162    }
163
164    EnumSet<Type> getBundleTypes() {
165        return bundleTypes;
166    }
167
168    String getCurrencies() {
169        return currencies;
170    }
171
172    /**
173     * Generate a map that contains all the data that should be
174     * visible for the bundle's locale
175     */
176    Map<String, Object> getTargetMap() throws Exception {
177        if (targetMap != null) {
178            return targetMap;
179        }
180
181        String[] cldrBundles = getCLDRPath().split(",");
182
183        // myMap contains resources for id.
184        Map<String, Object> myMap = new HashMap<>();
185        int index;
186        for (index = 0; index < cldrBundles.length; index++) {
187            if (cldrBundles[index].equals(id)) {
188                myMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[index]));
189                CLDRConverter.handleAliases(myMap);
190                break;
191            }
192        }
193
194        // parentsMap contains resources from id's parents.
195        Map<String, Object> parentsMap = new HashMap<>();
196        for (int i = cldrBundles.length - 1; i > index; i--) {
197            if (!("no".equals(cldrBundles[i]) || cldrBundles[i].startsWith("no_"))) {
198                parentsMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[i]));
199                CLDRConverter.handleAliases(parentsMap);
200            }
201        }
202        // Duplicate myMap as parentsMap for "root" so that the
203        // fallback works. This is a hack, though.
204        if ("root".equals(cldrBundles[0])) {
205            assert parentsMap.isEmpty();
206            parentsMap.putAll(myMap);
207        }
208
209        // merge individual strings into arrays
210
211        // if myMap has any of the NumberPatterns members
212        for (String k : NUMBER_PATTERN_KEYS) {
213            if (myMap.containsKey(k)) {
214                String[] numberPatterns = new String[NUMBER_PATTERN_KEYS.length];
215                for (int i = 0; i < NUMBER_PATTERN_KEYS.length; i++) {
216                    String key = NUMBER_PATTERN_KEYS[i];
217                    String value = (String) myMap.remove(key);
218                    if (value == null) {
219                        value = (String) parentsMap.remove(key);
220                    }
221                    if (value.length() == 0) {
222                        CLDRConverter.warning("empty pattern for " + key);
223                    }
224                    numberPatterns[i] = value;
225                }
226                myMap.put("NumberPatterns", numberPatterns);
227                break;
228            }
229        }
230
231        // if myMap has any of NUMBER_ELEMENT_KEYS, create a complete NumberElements.
232        String defaultScript = (String) myMap.get("DefaultNumberingSystem");
233        @SuppressWarnings("unchecked")
234        List<String> scripts = (List<String>) myMap.get("numberingScripts");
235        if (defaultScript == null && scripts != null) {
236            // Some locale data has no default script for numbering even with mutiple scripts.
237            // Take the first one as default in that case.
238            defaultScript = scripts.get(0);
239            myMap.put("DefaultNumberingSystem", defaultScript);
240        }
241        if (scripts != null) {
242            for (String script : scripts) {
243                for (String k : NUMBER_ELEMENT_KEYS) {
244                    String[] numberElements = new String[NUMBER_ELEMENT_KEYS.length];
245                    for (int i = 0; i < NUMBER_ELEMENT_KEYS.length; i++) {
246                        String key = script + "." + NUMBER_ELEMENT_KEYS[i];
247                        String value = (String) myMap.remove(key);
248                        if (value == null) {
249                            if (key.endsWith("/pattern")) {
250                                value = "#";
251                            } else {
252                                value = (String) parentsMap.get(key);
253                                if (value == null) {
254                                    // the last resort is "latn"
255                                    key = "latn." + NUMBER_ELEMENT_KEYS[i];
256                                    value = (String) parentsMap.get(key);
257                                    if (value == null) {
258                                        throw new InternalError("NumberElements: null for " + key);
259                                    }
260                                }
261                            }
262                        }
263                        numberElements[i] = value;
264                    }
265                    myMap.put(script + "." + "NumberElements", numberElements);
266                    break;
267                }
268            }
269        }
270
271        // another hack: parentsMap is not used for date-time resources.
272        if ("root".equals(id)) {
273            parentsMap = null;
274        }
275
276        for (CalendarType calendarType : CalendarType.values()) {
277            String calendarPrefix = calendarType.keyElementName();
278            // handle multiple inheritance for month and day names
279            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNames");
280            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthAbbreviations");
281            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNarrows");
282            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNames");
283            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayAbbreviations");
284            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNarrows");
285            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "AmPmMarkers");
286            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "narrow.AmPmMarkers");
287            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "abbreviated.AmPmMarkers");
288            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNames");
289            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterAbbreviations");
290            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNarrows");
291
292            adjustEraNames(myMap, calendarType);
293
294            handleDateTimeFormatPatterns(TIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "TimePatterns");
295            handleDateTimeFormatPatterns(DATE_PATTERN_KEYS, myMap, parentsMap, calendarType, "DatePatterns");
296            handleDateTimeFormatPatterns(DATETIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "DateTimePatterns");
297        }
298
299        // First, weed out any empty timezone or metazone names from myMap.
300        // Fill in any missing abbreviations if locale is "en".
301        for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
302            String key = it.next();
303            if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)
304                    || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
305                @SuppressWarnings("unchecked")
306                Map<String, String> nameMap = (Map<String, String>) myMap.get(key);
307                if (nameMap.isEmpty()) {
308                    // Some zones have only exemplarCity, which become empty.
309                    // Remove those from the map.
310                    it.remove();
311                    continue;
312                }
313
314                if (id.equals("en")) {
315                    fillInJREs(key, nameMap);
316                }
317            }
318        }
319        for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
320            String key = it.next();
321            if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)
322                    || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
323                @SuppressWarnings("unchecked")
324                Map<String, String> nameMap = (Map<String, String>) myMap.get(key);
325                // Convert key/value pairs to an array.
326                String[] names = new String[ZONE_NAME_KEYS.length];
327                int ix = 0;
328                for (String nameKey : ZONE_NAME_KEYS) {
329                    String name = nameMap.get(nameKey);
330                    if (name == null) {
331                        @SuppressWarnings("unchecked")
332                        Map<String, String> parentNames = (Map<String, String>) parentsMap.get(key);
333                        if (parentNames != null) {
334                            name = parentNames.get(nameKey);
335                        }
336                    }
337                    names[ix++] = name;
338                }
339                if (hasNulls(names)) {
340                    String metaKey = toMetaZoneKey(key);
341                    if (metaKey != null) {
342                        Object obj = myMap.get(metaKey);
343                        if (obj instanceof String[]) {
344                            String[] metaNames = (String[]) obj;
345                            for (int i = 0; i < names.length; i++) {
346                                if (names[i] == null) {
347                                    names[i] = metaNames[i];
348                                }
349                            }
350                        } else if (obj instanceof Map) {
351                            @SuppressWarnings("unchecked")
352                            Map<String, String> m = (Map<String, String>) obj;
353                            for (int i = 0; i < names.length; i++) {
354                                if (names[i] == null) {
355                                    names[i] = m.get(ZONE_NAME_KEYS[i]);
356                                }
357                            }
358                        }
359                    }
360                    // If there are still any nulls, try filling in them from en data.
361                    if (hasNulls(names) && !id.equals("en")) {
362                        @SuppressWarnings("unchecked")
363                        String[] enNames = (String[]) Bundle.getBundle("en").getTargetMap().get(key);
364                        if (enNames == null) {
365                            if (metaKey != null) {
366                                @SuppressWarnings("unchecked")
367                                String[] metaNames = (String[]) Bundle.getBundle("en").getTargetMap().get(metaKey);
368                                enNames = metaNames;
369                            }
370                        }
371                        if (enNames != null) {
372                            for (int i = 0; i < names.length; i++) {
373                                if (names[i] == null) {
374                                    names[i] = enNames[i];
375                                }
376                            }
377                        }
378                        // If there are still nulls, give up names.
379                        if (hasNulls(names)) {
380                            names = null;
381                        }
382                    }
383                }
384                // replace the Map with the array
385                if (names != null) {
386                    myMap.put(key, names);
387                } else {
388                    it.remove();
389                }
390            }
391        }
392        // replace empty era names with parentMap era names
393        for (String key : ERA_KEYS) {
394            Object value = myMap.get(key);
395            if (value != null && value instanceof String[]) {
396                String[] eraStrings = (String[]) value;
397                for (String eraString : eraStrings) {
398                    if (eraString == null || eraString.isEmpty()) {
399                        fillInElements(parentsMap, key, value);
400                    }
401                }
402            }
403        }
404
405        // Remove all duplicates
406        if (Objects.nonNull(parentsMap)) {
407            for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
408                String key = it.next();
409                if (!key.equals("numberingScripts") && // real body "NumberElements" may differ
410                    Objects.deepEquals(parentsMap.get(key), myMap.get(key))) {
411                    it.remove();
412                }
413            }
414        }
415
416        targetMap = myMap;
417        return myMap;
418    }
419
420    private void handleMultipleInheritance(Map<String, Object> map, Map<String, Object> parents, String key) {
421        String formatKey = key + "/format";
422        Object format = map.get(formatKey);
423        if (format != null) {
424            map.remove(formatKey);
425            map.put(key, format);
426            if (fillInElements(parents, formatKey, format)) {
427                map.remove(key);
428            }
429        }
430        String standaloneKey = key + "/stand-alone";
431        Object standalone = map.get(standaloneKey);
432        if (standalone != null) {
433            map.remove(standaloneKey);
434            String realKey = key;
435            if (format != null) {
436                realKey = "standalone." + key;
437            }
438            map.put(realKey, standalone);
439            if (fillInElements(parents, standaloneKey, standalone)) {
440                map.remove(realKey);
441            }
442        }
443    }
444
445    /**
446     * Fills in any empty elements with its parent element. Returns true if the resulting array is
447     * identical to its parent array.
448     *
449     * @param parents
450     * @param key
451     * @param value
452     * @return true if the resulting array is identical to its parent array.
453     */
454    private boolean fillInElements(Map<String, Object> parents, String key, Object value) {
455        if (parents == null) {
456            return false;
457        }
458        if (value instanceof String[]) {
459            Object pvalue = parents.get(key);
460            if (pvalue != null && pvalue instanceof String[]) {
461                String[] strings = (String[]) value;
462                String[] pstrings = (String[]) pvalue;
463                for (int i = 0; i < strings.length; i++) {
464                    if (strings[i] == null || strings[i].length() == 0) {
465                        strings[i] = pstrings[i];
466                    }
467                }
468                return Arrays.equals(strings, pstrings);
469            }
470        }
471        return false;
472    }
473
474    /*
475     * Adjusts String[] for era names because JRE's Calendars use different
476     * ERA value indexes in the Buddhist, Japanese Imperial, and Islamic calendars.
477     */
478    private void adjustEraNames(Map<String, Object> map, CalendarType type) {
479        String[][] eraNames = new String[ERA_KEYS.length][];
480        String[] realKeys = new String[ERA_KEYS.length];
481        int index = 0;
482        for (String key : ERA_KEYS) {
483            String realKey = type.keyElementName() + key;
484            String[] value = (String[]) map.get(realKey);
485            if (value != null) {
486                switch (type) {
487                case GREGORIAN:
488                    break;
489
490                case JAPANESE:
491                    {
492                        String[] newValue = new String[value.length + 1];
493                        String[] julianEras = (String[]) map.get(key);
494                        if (julianEras != null && julianEras.length >= 2) {
495                            newValue[0] = julianEras[1];
496                        } else {
497                            newValue[0] = "";
498                        }
499                        System.arraycopy(value, 0, newValue, 1, value.length);
500                        value = newValue;
501                    }
502                    break;
503
504                case BUDDHIST:
505                    // Replace the value
506                    value = new String[] {"BC", value[0]};
507                    break;
508
509                case ISLAMIC:
510                    // Replace the value
511                    value = new String[] {"", value[0]};
512                    break;
513                }
514                if (!key.equals(realKey)) {
515                    map.put(realKey, value);
516                }
517            }
518            realKeys[index] = realKey;
519            eraNames[index++] = value;
520        }
521        for (int i = 0; i < eraNames.length; i++) {
522            if (eraNames[i] == null) {
523                map.put(realKeys[i], null);
524            }
525        }
526    }
527
528    private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap,
529                                              CalendarType calendarType, String name) {
530        String calendarPrefix = calendarType.keyElementName();
531        for (String k : patternKeys) {
532            if (myMap.containsKey(calendarPrefix + k)) {
533                int len = patternKeys.length;
534                List<String> rawPatterns = new ArrayList<>(len);
535                List<String> patterns = new ArrayList<>(len);
536                for (int i = 0; i < len; i++) {
537                    String key = calendarPrefix + patternKeys[i];
538                    String pattern = (String) myMap.remove(key);
539                    if (pattern == null) {
540                        pattern = (String) parentsMap.remove(key);
541                    }
542                    rawPatterns.add(i, pattern);
543                    if (pattern != null) {
544                        patterns.add(i, translateDateFormatLetters(calendarType, pattern));
545                    } else {
546                        patterns.add(i, null);
547                    }
548                }
549                // If patterns is empty or has any nulls, discard patterns.
550                if (patterns.isEmpty()) {
551                    return;
552                }
553                String key = calendarPrefix + name;
554                if (!rawPatterns.equals(patterns)) {
555                    myMap.put("java.time." + key, rawPatterns.toArray(new String[len]));
556                }
557                myMap.put(key, patterns.toArray(new String[len]));
558                break;
559            }
560        }
561    }
562
563    private String translateDateFormatLetters(CalendarType calendarType, String cldrFormat) {
564        String pattern = cldrFormat;
565        int length = pattern.length();
566        boolean inQuote = false;
567        StringBuilder jrePattern = new StringBuilder(length);
568        int count = 0;
569        char lastLetter = 0;
570
571        for (int i = 0; i < length; i++) {
572            char c = pattern.charAt(i);
573
574            if (c == '\'') {
575                // '' is treated as a single quote regardless of being
576                // in a quoted section.
577                if ((i + 1) < length) {
578                    char nextc = pattern.charAt(i + 1);
579                    if (nextc == '\'') {
580                        i++;
581                        if (count != 0) {
582                            convert(calendarType, lastLetter, count, jrePattern);
583                            lastLetter = 0;
584                            count = 0;
585                        }
586                        jrePattern.append("''");
587                        continue;
588                    }
589                }
590                if (!inQuote) {
591                    if (count != 0) {
592                        convert(calendarType, lastLetter, count, jrePattern);
593                        lastLetter = 0;
594                        count = 0;
595                    }
596                    inQuote = true;
597                } else {
598                    inQuote = false;
599                }
600                jrePattern.append(c);
601                continue;
602            }
603            if (inQuote) {
604                jrePattern.append(c);
605                continue;
606            }
607            if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
608                if (count != 0) {
609                    convert(calendarType, lastLetter, count, jrePattern);
610                    lastLetter = 0;
611                    count = 0;
612                }
613                jrePattern.append(c);
614                continue;
615            }
616
617            if (lastLetter == 0 || lastLetter == c) {
618                lastLetter = c;
619                count++;
620                continue;
621            }
622            convert(calendarType, lastLetter, count, jrePattern);
623            lastLetter = c;
624            count = 1;
625        }
626
627        if (inQuote) {
628            throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat);
629        }
630
631        if (count != 0) {
632            convert(calendarType, lastLetter, count, jrePattern);
633        }
634        if (cldrFormat.contentEquals(jrePattern)) {
635            return cldrFormat;
636        }
637        return jrePattern.toString();
638    }
639
640    private String toMetaZoneKey(String tzKey) {
641        if (tzKey.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)) {
642            String tz = tzKey.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length());
643            String meta = CLDRConverter.handlerMetaZones.get(tz);
644            if (meta != null) {
645                return CLDRConverter.METAZONE_ID_PREFIX + meta;
646            }
647        }
648        return null;
649    }
650
651    static List<Object[]> jreTimeZoneNames = Arrays.asList(TimeZoneNames.getContents());
652    private void fillInJREs(String key, Map<String, String> map) {
653        String tzid = null;
654
655        if (key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
656            // Look for tzid
657            String meta = key.substring(CLDRConverter.METAZONE_ID_PREFIX.length());
658            if (meta.equals("GMT")) {
659                tzid = meta;
660            } else {
661                for (String tz : CLDRConverter.handlerMetaZones.keySet()) {
662                    if (CLDRConverter.handlerMetaZones.get(tz).equals(meta)) {
663                        tzid = tz;
664                        break;
665                        }
666                    }
667                }
668        } else {
669            tzid = key.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length());
670    }
671
672        if (tzid != null) {
673            for (Object[] jreZone : jreTimeZoneNames) {
674                if (jreZone[0].equals(tzid)) {
675                    for (int i = 0; i < ZONE_NAME_KEYS.length; i++) {
676                        if (map.get(ZONE_NAME_KEYS[i]) == null) {
677                            String[] jreNames = (String[])jreZone[1];
678                            map.put(ZONE_NAME_KEYS[i], jreNames[i]);
679                }
680            }
681                    break;
682        }
683    }
684            }
685        }
686
687    private void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) {
688        switch (cldrLetter) {
689        case 'G':
690            if (calendarType != CalendarType.GREGORIAN) {
691                // Adjust the number of 'G's for JRE SimpleDateFormat
692                if (count == 5) {
693                    // CLDR narrow -> JRE short
694                    count = 1;
695                } else if (count == 1) {
696                    // CLDR abbr -> JRE long
697                    count = 4;
698                }
699            }
700            appendN(cldrLetter, count, sb);
701            break;
702
703        // TODO: support 'c' and 'e' in JRE SimpleDateFormat
704        // Use 'u' and 'E' for now.
705        case 'c':
706        case 'e':
707            switch (count) {
708            case 1:
709                sb.append('u');
710                break;
711            case 3:
712            case 4:
713                appendN('E', count, sb);
714                break;
715            case 5:
716                appendN('E', 3, sb);
717                break;
718            }
719            break;
720
721        case 'l':
722            // 'l' is deprecated as a pattern character. Should be ignored.
723            break;
724
725        case 'u':
726            // Use 'y' for now.
727            appendN('y', count, sb);
728            break;
729
730        case 'v':
731        case 'V':
732            appendN('z', count, sb);
733            break;
734
735        case 'Z':
736            if (count == 4 || count == 5) {
737                sb.append("XXX");
738            }
739            break;
740
741        case 'U':
742        case 'q':
743        case 'Q':
744        case 'g':
745        case 'j':
746        case 'A':
747            throw new InternalError(String.format("Unsupported letter: '%c', count=%d, id=%s%n",
748                                                  cldrLetter, count, id));
749        default:
750            appendN(cldrLetter, count, sb);
751            break;
752        }
753    }
754
755    private void appendN(char c, int n, StringBuilder sb) {
756        for (int i = 0; i < n; i++) {
757            sb.append(c);
758        }
759    }
760
761    private static boolean hasNulls(Object[] array) {
762        for (int i = 0; i < array.length; i++) {
763            if (array[i] == null) {
764                return true;
765            }
766        }
767        return false;
768    }
769}
770