make/jdk/src/classes/build/tools/cldrconverter/Bundle.java
changeset 47216 71c04702a3d5
parent 43295 58870c7a62e0
child 49904 cadca99d52e7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java	Tue Sep 12 19:03:39 2017 +0200
@@ -0,0 +1,769 @@
+/*
+ * Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package build.tools.cldrconverter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+class Bundle {
+    static enum Type {
+        LOCALENAMES, CURRENCYNAMES, TIMEZONENAMES, CALENDARDATA, FORMATDATA;
+
+        static EnumSet<Type> ALL_TYPES = EnumSet.of(LOCALENAMES,
+                                                    CURRENCYNAMES,
+                                                    TIMEZONENAMES,
+                                                    CALENDARDATA,
+                                                    FORMATDATA);
+    }
+
+    private final static Map<String, Bundle> bundles = new HashMap<>();
+
+    private final static String[] NUMBER_PATTERN_KEYS = {
+        "NumberPatterns/decimal",
+        "NumberPatterns/currency",
+        "NumberPatterns/percent"
+    };
+
+    private final static String[] NUMBER_ELEMENT_KEYS = {
+        "NumberElements/decimal",
+        "NumberElements/group",
+        "NumberElements/list",
+        "NumberElements/percent",
+        "NumberElements/zero",
+        "NumberElements/pattern",
+        "NumberElements/minus",
+        "NumberElements/exponential",
+        "NumberElements/permille",
+        "NumberElements/infinity",
+        "NumberElements/nan"
+    };
+
+    private final static String[] TIME_PATTERN_KEYS = {
+        "DateTimePatterns/full-time",
+        "DateTimePatterns/long-time",
+        "DateTimePatterns/medium-time",
+        "DateTimePatterns/short-time",
+    };
+
+    private final static String[] DATE_PATTERN_KEYS = {
+        "DateTimePatterns/full-date",
+        "DateTimePatterns/long-date",
+        "DateTimePatterns/medium-date",
+        "DateTimePatterns/short-date",
+    };
+
+    private final static String[] DATETIME_PATTERN_KEYS = {
+        "DateTimePatterns/full-dateTime",
+        "DateTimePatterns/long-dateTime",
+        "DateTimePatterns/medium-dateTime",
+        "DateTimePatterns/short-dateTime",
+    };
+
+    private final static String[] ERA_KEYS = {
+        "long.Eras",
+        "Eras",
+        "narrow.Eras"
+    };
+
+    // Keys for individual time zone names
+    private final static String TZ_GEN_LONG_KEY = "timezone.displayname.generic.long";
+    private final static String TZ_GEN_SHORT_KEY = "timezone.displayname.generic.short";
+    private final static String TZ_STD_LONG_KEY = "timezone.displayname.standard.long";
+    private final static String TZ_STD_SHORT_KEY = "timezone.displayname.standard.short";
+    private final static String TZ_DST_LONG_KEY = "timezone.displayname.daylight.long";
+    private final static String TZ_DST_SHORT_KEY = "timezone.displayname.daylight.short";
+    private final static String[] ZONE_NAME_KEYS = {
+        TZ_STD_LONG_KEY,
+        TZ_STD_SHORT_KEY,
+        TZ_DST_LONG_KEY,
+        TZ_DST_SHORT_KEY,
+        TZ_GEN_LONG_KEY,
+        TZ_GEN_SHORT_KEY
+    };
+
+    private final String id;
+    private final String cldrPath;
+    private final EnumSet<Type> bundleTypes;
+    private final String currencies;
+    private Map<String, Object> targetMap;
+
+    static Bundle getBundle(String id) {
+        return bundles.get(id);
+    }
+
+    @SuppressWarnings("ConvertToStringSwitch")
+    Bundle(String id, String cldrPath, String bundles, String currencies) {
+        this.id = id;
+        this.cldrPath = cldrPath;
+        if ("localenames".equals(bundles)) {
+            bundleTypes = EnumSet.of(Type.LOCALENAMES);
+        } else if ("currencynames".equals(bundles)) {
+            bundleTypes = EnumSet.of(Type.CURRENCYNAMES);
+        } else {
+            bundleTypes = Type.ALL_TYPES;
+        }
+        if (currencies == null) {
+            currencies = "local";
+        }
+        this.currencies = currencies;
+        addBundle();
+    }
+
+    private void addBundle() {
+        Bundle.bundles.put(id, this);
+    }
+
+    String getID() {
+        return id;
+    }
+
+    String getJavaID() {
+        // Tweak ISO compatibility for bundle generation
+        return id.replaceFirst("^he", "iw")
+            .replaceFirst("^id", "in")
+            .replaceFirst("^yi", "ji");
+    }
+
+    boolean isRoot() {
+        return "root".equals(id);
+    }
+
+    String getCLDRPath() {
+        return cldrPath;
+    }
+
+    EnumSet<Type> getBundleTypes() {
+        return bundleTypes;
+    }
+
+    String getCurrencies() {
+        return currencies;
+    }
+
+    /**
+     * Generate a map that contains all the data that should be
+     * visible for the bundle's locale
+     */
+    Map<String, Object> getTargetMap() throws Exception {
+        if (targetMap != null) {
+            return targetMap;
+        }
+
+        String[] cldrBundles = getCLDRPath().split(",");
+
+        // myMap contains resources for id.
+        Map<String, Object> myMap = new HashMap<>();
+        int index;
+        for (index = 0; index < cldrBundles.length; index++) {
+            if (cldrBundles[index].equals(id)) {
+                myMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[index]));
+                CLDRConverter.handleAliases(myMap);
+                break;
+            }
+        }
+
+        // parentsMap contains resources from id's parents.
+        Map<String, Object> parentsMap = new HashMap<>();
+        for (int i = cldrBundles.length - 1; i > index; i--) {
+            if (!("no".equals(cldrBundles[i]) || cldrBundles[i].startsWith("no_"))) {
+                parentsMap.putAll(CLDRConverter.getCLDRBundle(cldrBundles[i]));
+                CLDRConverter.handleAliases(parentsMap);
+            }
+        }
+        // Duplicate myMap as parentsMap for "root" so that the
+        // fallback works. This is a hack, though.
+        if ("root".equals(cldrBundles[0])) {
+            assert parentsMap.isEmpty();
+            parentsMap.putAll(myMap);
+        }
+
+        // merge individual strings into arrays
+
+        // if myMap has any of the NumberPatterns members
+        for (String k : NUMBER_PATTERN_KEYS) {
+            if (myMap.containsKey(k)) {
+                String[] numberPatterns = new String[NUMBER_PATTERN_KEYS.length];
+                for (int i = 0; i < NUMBER_PATTERN_KEYS.length; i++) {
+                    String key = NUMBER_PATTERN_KEYS[i];
+                    String value = (String) myMap.remove(key);
+                    if (value == null) {
+                        value = (String) parentsMap.remove(key);
+                    }
+                    if (value.length() == 0) {
+                        CLDRConverter.warning("empty pattern for " + key);
+                    }
+                    numberPatterns[i] = value;
+                }
+                myMap.put("NumberPatterns", numberPatterns);
+                break;
+            }
+        }
+
+        // if myMap has any of NUMBER_ELEMENT_KEYS, create a complete NumberElements.
+        String defaultScript = (String) myMap.get("DefaultNumberingSystem");
+        @SuppressWarnings("unchecked")
+        List<String> scripts = (List<String>) myMap.get("numberingScripts");
+        if (defaultScript == null && scripts != null) {
+            // Some locale data has no default script for numbering even with mutiple scripts.
+            // Take the first one as default in that case.
+            defaultScript = scripts.get(0);
+            myMap.put("DefaultNumberingSystem", defaultScript);
+        }
+        if (scripts != null) {
+            for (String script : scripts) {
+                for (String k : NUMBER_ELEMENT_KEYS) {
+                    String[] numberElements = new String[NUMBER_ELEMENT_KEYS.length];
+                    for (int i = 0; i < NUMBER_ELEMENT_KEYS.length; i++) {
+                        String key = script + "." + NUMBER_ELEMENT_KEYS[i];
+                        String value = (String) myMap.remove(key);
+                        if (value == null) {
+                            if (key.endsWith("/pattern")) {
+                                value = "#";
+                            } else {
+                                value = (String) parentsMap.get(key);
+                                if (value == null) {
+                                    // the last resort is "latn"
+                                    key = "latn." + NUMBER_ELEMENT_KEYS[i];
+                                    value = (String) parentsMap.get(key);
+                                    if (value == null) {
+                                        throw new InternalError("NumberElements: null for " + key);
+                                    }
+                                }
+                            }
+                        }
+                        numberElements[i] = value;
+                    }
+                    myMap.put(script + "." + "NumberElements", numberElements);
+                    break;
+                }
+            }
+        }
+
+        // another hack: parentsMap is not used for date-time resources.
+        if ("root".equals(id)) {
+            parentsMap = null;
+        }
+
+        for (CalendarType calendarType : CalendarType.values()) {
+            String calendarPrefix = calendarType.keyElementName();
+            // handle multiple inheritance for month and day names
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNames");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthAbbreviations");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "MonthNarrows");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNames");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayAbbreviations");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "DayNarrows");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "AmPmMarkers");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "narrow.AmPmMarkers");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "abbreviated.AmPmMarkers");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNames");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterAbbreviations");
+            handleMultipleInheritance(myMap, parentsMap, calendarPrefix + "QuarterNarrows");
+
+            adjustEraNames(myMap, calendarType);
+
+            handleDateTimeFormatPatterns(TIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "TimePatterns");
+            handleDateTimeFormatPatterns(DATE_PATTERN_KEYS, myMap, parentsMap, calendarType, "DatePatterns");
+            handleDateTimeFormatPatterns(DATETIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "DateTimePatterns");
+        }
+
+        // First, weed out any empty timezone or metazone names from myMap.
+        // Fill in any missing abbreviations if locale is "en".
+        for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
+            String key = it.next();
+            if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)
+                    || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
+                @SuppressWarnings("unchecked")
+                Map<String, String> nameMap = (Map<String, String>) myMap.get(key);
+                if (nameMap.isEmpty()) {
+                    // Some zones have only exemplarCity, which become empty.
+                    // Remove those from the map.
+                    it.remove();
+                    continue;
+                }
+
+                if (id.equals("en")) {
+                    fillInJREs(key, nameMap);
+                }
+            }
+        }
+        for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
+            String key = it.next();
+            if (key.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)
+                    || key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
+                @SuppressWarnings("unchecked")
+                Map<String, String> nameMap = (Map<String, String>) myMap.get(key);
+                // Convert key/value pairs to an array.
+                String[] names = new String[ZONE_NAME_KEYS.length];
+                int ix = 0;
+                for (String nameKey : ZONE_NAME_KEYS) {
+                    String name = nameMap.get(nameKey);
+                    if (name == null) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, String> parentNames = (Map<String, String>) parentsMap.get(key);
+                        if (parentNames != null) {
+                            name = parentNames.get(nameKey);
+                        }
+                    }
+                    names[ix++] = name;
+                }
+                if (hasNulls(names)) {
+                    String metaKey = toMetaZoneKey(key);
+                    if (metaKey != null) {
+                        Object obj = myMap.get(metaKey);
+                        if (obj instanceof String[]) {
+                            String[] metaNames = (String[]) obj;
+                            for (int i = 0; i < names.length; i++) {
+                                if (names[i] == null) {
+                                    names[i] = metaNames[i];
+                                }
+                            }
+                        } else if (obj instanceof Map) {
+                            @SuppressWarnings("unchecked")
+                            Map<String, String> m = (Map<String, String>) obj;
+                            for (int i = 0; i < names.length; i++) {
+                                if (names[i] == null) {
+                                    names[i] = m.get(ZONE_NAME_KEYS[i]);
+                                }
+                            }
+                        }
+                    }
+                    // If there are still any nulls, try filling in them from en data.
+                    if (hasNulls(names) && !id.equals("en")) {
+                        @SuppressWarnings("unchecked")
+                        String[] enNames = (String[]) Bundle.getBundle("en").getTargetMap().get(key);
+                        if (enNames == null) {
+                            if (metaKey != null) {
+                                @SuppressWarnings("unchecked")
+                                String[] metaNames = (String[]) Bundle.getBundle("en").getTargetMap().get(metaKey);
+                                enNames = metaNames;
+                            }
+                        }
+                        if (enNames != null) {
+                            for (int i = 0; i < names.length; i++) {
+                                if (names[i] == null) {
+                                    names[i] = enNames[i];
+                                }
+                            }
+                        }
+                        // If there are still nulls, give up names.
+                        if (hasNulls(names)) {
+                            names = null;
+                        }
+                    }
+                }
+                // replace the Map with the array
+                if (names != null) {
+                    myMap.put(key, names);
+                } else {
+                    it.remove();
+                }
+            }
+        }
+        // replace empty era names with parentMap era names
+        for (String key : ERA_KEYS) {
+            Object value = myMap.get(key);
+            if (value != null && value instanceof String[]) {
+                String[] eraStrings = (String[]) value;
+                for (String eraString : eraStrings) {
+                    if (eraString == null || eraString.isEmpty()) {
+                        fillInElements(parentsMap, key, value);
+                    }
+                }
+            }
+        }
+
+        // Remove all duplicates
+        if (Objects.nonNull(parentsMap)) {
+            for (Iterator<String> it = myMap.keySet().iterator(); it.hasNext();) {
+                String key = it.next();
+                if (!key.equals("numberingScripts") && // real body "NumberElements" may differ
+                    Objects.deepEquals(parentsMap.get(key), myMap.get(key))) {
+                    it.remove();
+                }
+            }
+        }
+
+        targetMap = myMap;
+        return myMap;
+    }
+
+    private void handleMultipleInheritance(Map<String, Object> map, Map<String, Object> parents, String key) {
+        String formatKey = key + "/format";
+        Object format = map.get(formatKey);
+        if (format != null) {
+            map.remove(formatKey);
+            map.put(key, format);
+            if (fillInElements(parents, formatKey, format)) {
+                map.remove(key);
+            }
+        }
+        String standaloneKey = key + "/stand-alone";
+        Object standalone = map.get(standaloneKey);
+        if (standalone != null) {
+            map.remove(standaloneKey);
+            String realKey = key;
+            if (format != null) {
+                realKey = "standalone." + key;
+            }
+            map.put(realKey, standalone);
+            if (fillInElements(parents, standaloneKey, standalone)) {
+                map.remove(realKey);
+            }
+        }
+    }
+
+    /**
+     * Fills in any empty elements with its parent element. Returns true if the resulting array is
+     * identical to its parent array.
+     *
+     * @param parents
+     * @param key
+     * @param value
+     * @return true if the resulting array is identical to its parent array.
+     */
+    private boolean fillInElements(Map<String, Object> parents, String key, Object value) {
+        if (parents == null) {
+            return false;
+        }
+        if (value instanceof String[]) {
+            Object pvalue = parents.get(key);
+            if (pvalue != null && pvalue instanceof String[]) {
+                String[] strings = (String[]) value;
+                String[] pstrings = (String[]) pvalue;
+                for (int i = 0; i < strings.length; i++) {
+                    if (strings[i] == null || strings[i].length() == 0) {
+                        strings[i] = pstrings[i];
+                    }
+                }
+                return Arrays.equals(strings, pstrings);
+            }
+        }
+        return false;
+    }
+
+    /*
+     * Adjusts String[] for era names because JRE's Calendars use different
+     * ERA value indexes in the Buddhist, Japanese Imperial, and Islamic calendars.
+     */
+    private void adjustEraNames(Map<String, Object> map, CalendarType type) {
+        String[][] eraNames = new String[ERA_KEYS.length][];
+        String[] realKeys = new String[ERA_KEYS.length];
+        int index = 0;
+        for (String key : ERA_KEYS) {
+            String realKey = type.keyElementName() + key;
+            String[] value = (String[]) map.get(realKey);
+            if (value != null) {
+                switch (type) {
+                case GREGORIAN:
+                    break;
+
+                case JAPANESE:
+                    {
+                        String[] newValue = new String[value.length + 1];
+                        String[] julianEras = (String[]) map.get(key);
+                        if (julianEras != null && julianEras.length >= 2) {
+                            newValue[0] = julianEras[1];
+                        } else {
+                            newValue[0] = "";
+                        }
+                        System.arraycopy(value, 0, newValue, 1, value.length);
+                        value = newValue;
+                    }
+                    break;
+
+                case BUDDHIST:
+                    // Replace the value
+                    value = new String[] {"BC", value[0]};
+                    break;
+
+                case ISLAMIC:
+                    // Replace the value
+                    value = new String[] {"", value[0]};
+                    break;
+                }
+                if (!key.equals(realKey)) {
+                    map.put(realKey, value);
+                }
+            }
+            realKeys[index] = realKey;
+            eraNames[index++] = value;
+        }
+        for (int i = 0; i < eraNames.length; i++) {
+            if (eraNames[i] == null) {
+                map.put(realKeys[i], null);
+            }
+        }
+    }
+
+    private void handleDateTimeFormatPatterns(String[] patternKeys, Map<String, Object> myMap, Map<String, Object> parentsMap,
+                                              CalendarType calendarType, String name) {
+        String calendarPrefix = calendarType.keyElementName();
+        for (String k : patternKeys) {
+            if (myMap.containsKey(calendarPrefix + k)) {
+                int len = patternKeys.length;
+                List<String> rawPatterns = new ArrayList<>(len);
+                List<String> patterns = new ArrayList<>(len);
+                for (int i = 0; i < len; i++) {
+                    String key = calendarPrefix + patternKeys[i];
+                    String pattern = (String) myMap.remove(key);
+                    if (pattern == null) {
+                        pattern = (String) parentsMap.remove(key);
+                    }
+                    rawPatterns.add(i, pattern);
+                    if (pattern != null) {
+                        patterns.add(i, translateDateFormatLetters(calendarType, pattern));
+                    } else {
+                        patterns.add(i, null);
+                    }
+                }
+                // If patterns is empty or has any nulls, discard patterns.
+                if (patterns.isEmpty()) {
+                    return;
+                }
+                String key = calendarPrefix + name;
+                if (!rawPatterns.equals(patterns)) {
+                    myMap.put("java.time." + key, rawPatterns.toArray(new String[len]));
+                }
+                myMap.put(key, patterns.toArray(new String[len]));
+                break;
+            }
+        }
+    }
+
+    private String translateDateFormatLetters(CalendarType calendarType, String cldrFormat) {
+        String pattern = cldrFormat;
+        int length = pattern.length();
+        boolean inQuote = false;
+        StringBuilder jrePattern = new StringBuilder(length);
+        int count = 0;
+        char lastLetter = 0;
+
+        for (int i = 0; i < length; i++) {
+            char c = pattern.charAt(i);
+
+            if (c == '\'') {
+                // '' is treated as a single quote regardless of being
+                // in a quoted section.
+                if ((i + 1) < length) {
+                    char nextc = pattern.charAt(i + 1);
+                    if (nextc == '\'') {
+                        i++;
+                        if (count != 0) {
+                            convert(calendarType, lastLetter, count, jrePattern);
+                            lastLetter = 0;
+                            count = 0;
+                        }
+                        jrePattern.append("''");
+                        continue;
+                    }
+                }
+                if (!inQuote) {
+                    if (count != 0) {
+                        convert(calendarType, lastLetter, count, jrePattern);
+                        lastLetter = 0;
+                        count = 0;
+                    }
+                    inQuote = true;
+                } else {
+                    inQuote = false;
+                }
+                jrePattern.append(c);
+                continue;
+            }
+            if (inQuote) {
+                jrePattern.append(c);
+                continue;
+            }
+            if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
+                if (count != 0) {
+                    convert(calendarType, lastLetter, count, jrePattern);
+                    lastLetter = 0;
+                    count = 0;
+                }
+                jrePattern.append(c);
+                continue;
+            }
+
+            if (lastLetter == 0 || lastLetter == c) {
+                lastLetter = c;
+                count++;
+                continue;
+            }
+            convert(calendarType, lastLetter, count, jrePattern);
+            lastLetter = c;
+            count = 1;
+        }
+
+        if (inQuote) {
+            throw new InternalError("Unterminated quote in date-time pattern: " + cldrFormat);
+        }
+
+        if (count != 0) {
+            convert(calendarType, lastLetter, count, jrePattern);
+        }
+        if (cldrFormat.contentEquals(jrePattern)) {
+            return cldrFormat;
+        }
+        return jrePattern.toString();
+    }
+
+    private String toMetaZoneKey(String tzKey) {
+        if (tzKey.startsWith(CLDRConverter.TIMEZONE_ID_PREFIX)) {
+            String tz = tzKey.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length());
+            String meta = CLDRConverter.handlerMetaZones.get(tz);
+            if (meta != null) {
+                return CLDRConverter.METAZONE_ID_PREFIX + meta;
+            }
+        }
+        return null;
+    }
+
+    static List<Object[]> jreTimeZoneNames = Arrays.asList(TimeZoneNames.getContents());
+    private void fillInJREs(String key, Map<String, String> map) {
+        String tzid = null;
+
+        if (key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
+            // Look for tzid
+            String meta = key.substring(CLDRConverter.METAZONE_ID_PREFIX.length());
+            if (meta.equals("GMT")) {
+                tzid = meta;
+            } else {
+                for (String tz : CLDRConverter.handlerMetaZones.keySet()) {
+                    if (CLDRConverter.handlerMetaZones.get(tz).equals(meta)) {
+                        tzid = tz;
+                        break;
+                        }
+                    }
+                }
+        } else {
+            tzid = key.substring(CLDRConverter.TIMEZONE_ID_PREFIX.length());
+    }
+
+        if (tzid != null) {
+            for (Object[] jreZone : jreTimeZoneNames) {
+                if (jreZone[0].equals(tzid)) {
+                    for (int i = 0; i < ZONE_NAME_KEYS.length; i++) {
+                        if (map.get(ZONE_NAME_KEYS[i]) == null) {
+                            String[] jreNames = (String[])jreZone[1];
+                            map.put(ZONE_NAME_KEYS[i], jreNames[i]);
+                }
+            }
+                    break;
+        }
+    }
+            }
+        }
+
+    private void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) {
+        switch (cldrLetter) {
+        case 'G':
+            if (calendarType != CalendarType.GREGORIAN) {
+                // Adjust the number of 'G's for JRE SimpleDateFormat
+                if (count == 5) {
+                    // CLDR narrow -> JRE short
+                    count = 1;
+                } else if (count == 1) {
+                    // CLDR abbr -> JRE long
+                    count = 4;
+                }
+            }
+            appendN(cldrLetter, count, sb);
+            break;
+
+        // TODO: support 'c' and 'e' in JRE SimpleDateFormat
+        // Use 'u' and 'E' for now.
+        case 'c':
+        case 'e':
+            switch (count) {
+            case 1:
+                sb.append('u');
+                break;
+            case 3:
+            case 4:
+                appendN('E', count, sb);
+                break;
+            case 5:
+                appendN('E', 3, sb);
+                break;
+            }
+            break;
+
+        case 'l':
+            // 'l' is deprecated as a pattern character. Should be ignored.
+            break;
+
+        case 'u':
+            // Use 'y' for now.
+            appendN('y', count, sb);
+            break;
+
+        case 'v':
+        case 'V':
+            appendN('z', count, sb);
+            break;
+
+        case 'Z':
+            if (count == 4 || count == 5) {
+                sb.append("XXX");
+            }
+            break;
+
+        case 'U':
+        case 'q':
+        case 'Q':
+        case 'g':
+        case 'j':
+        case 'A':
+            throw new InternalError(String.format("Unsupported letter: '%c', count=%d, id=%s%n",
+                                                  cldrLetter, count, id));
+        default:
+            appendN(cldrLetter, count, sb);
+            break;
+        }
+    }
+
+    private void appendN(char c, int n, StringBuilder sb) {
+        for (int i = 0; i < n; i++) {
+            sb.append(c);
+        }
+    }
+
+    private static boolean hasNulls(Object[] array) {
+        for (int i = 0; i < array.length; i++) {
+            if (array[i] == null) {
+                return true;
+            }
+        }
+        return false;
+    }
+}