CLDRConverter.java revision 8845:4be14673b9bf
1/*
2 * Copyright (c) 2012, 2013, 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 build.tools.cldrconverter.BundleGenerator.BundleType;
29import java.io.File;
30import java.nio.file.DirectoryStream;
31import java.nio.file.FileSystems;
32import java.nio.file.Files;
33import java.nio.file.Path;
34import java.util.*;
35import javax.xml.parsers.SAXParser;
36import javax.xml.parsers.SAXParserFactory;
37import org.xml.sax.SAXNotRecognizedException;
38import org.xml.sax.SAXNotSupportedException;
39
40
41/**
42 * Converts locale data from "Locale Data Markup Language" format to
43 * JRE resource bundle format. LDML is the format used by the Common
44 * Locale Data Repository maintained by the Unicode Consortium.
45 */
46public class CLDRConverter {
47
48    static final String LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldml.dtd";
49    static final String SPPL_LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldmlSupplemental.dtd";
50
51    private static String CLDR_BASE = "../CLDR/21.0.1/";
52    static String LOCAL_LDML_DTD;
53    static String LOCAL_SPPL_LDML_DTD;
54    private static String SOURCE_FILE_DIR;
55    private static String SPPL_SOURCE_FILE;
56    private static String NUMBERING_SOURCE_FILE;
57    private static String METAZONES_SOURCE_FILE;
58    static String DESTINATION_DIR = "build/gensrc";
59
60    static final String LOCALE_NAME_PREFIX = "locale.displayname.";
61    static final String CURRENCY_SYMBOL_PREFIX = "currency.symbol.";
62    static final String CURRENCY_NAME_PREFIX = "currency.displayname.";
63    static final String CALENDAR_NAME_PREFIX = "calendarname.";
64    static final String TIMEZONE_ID_PREFIX = "timezone.id.";
65    static final String ZONE_NAME_PREFIX = "timezone.displayname.";
66    static final String METAZONE_ID_PREFIX = "metazone.id.";
67
68    private static SupplementDataParseHandler handlerSuppl;
69    static NumberingSystemsParseHandler handlerNumbering;
70    static MetaZonesParseHandler handlerMetaZones;
71    private static BundleGenerator bundleGenerator;
72
73    static enum DraftType {
74        UNCONFIRMED,
75        PROVISIONAL,
76        CONTRIBUTED,
77        APPROVED;
78
79        private static final Map<String, DraftType> map = new HashMap<>();
80        static {
81            for (DraftType dt : values()) {
82                map.put(dt.getKeyword(), dt);
83            }
84        }
85        static private DraftType defaultType = CONTRIBUTED;
86
87        private final String keyword;
88
89        private DraftType() {
90            keyword = this.name().toLowerCase(Locale.ROOT);
91
92        }
93
94        static DraftType forKeyword(String keyword) {
95            return map.get(keyword);
96        }
97
98        static DraftType getDefault() {
99            return defaultType;
100        }
101
102        static void setDefault(String keyword) {
103            defaultType = Objects.requireNonNull(forKeyword(keyword));
104        }
105
106        String getKeyword() {
107            return keyword;
108        }
109    }
110
111    static boolean USE_UTF8 = false;
112    private static boolean verbose;
113
114    private CLDRConverter() {
115       // no instantiation
116    }
117
118    @SuppressWarnings("AssignmentToForLoopParameter")
119    public static void main(String[] args) throws Exception {
120        if (args.length != 0) {
121            String currentArg = null;
122            try {
123                for (int i = 0; i < args.length; i++) {
124                    currentArg = args[i];
125                    switch (currentArg) {
126                    case "-draft":
127                        String draftDataType = args[++i];
128                        try {
129                            DraftType.setDefault(draftDataType);
130                        } catch (NullPointerException e) {
131                            severe("Error: incorrect draft value: %s%n", draftDataType);
132                            System.exit(1);
133                        }
134                        info("Using the specified data type: %s%n", draftDataType);
135                        break;
136
137                    case "-base":
138                        // base directory for input files
139                        CLDR_BASE = args[++i];
140                        if (!CLDR_BASE.endsWith("/")) {
141                            CLDR_BASE += "/";
142                        }
143                        break;
144
145                    case "-o":
146                        // output directory
147                        DESTINATION_DIR = args[++i];
148                        break;
149
150                    case "-utf8":
151                        USE_UTF8 = true;
152                        break;
153
154                    case "-verbose":
155                        verbose = true;
156                        break;
157
158                    case "-help":
159                        usage();
160                        System.exit(0);
161                        break;
162
163                    default:
164                        throw new RuntimeException();
165                    }
166                }
167            } catch (RuntimeException e) {
168                severe("unknown or imcomplete arg(s): " + currentArg);
169                usage();
170                System.exit(1);
171            }
172        }
173
174        // Set up path names
175        LOCAL_LDML_DTD = CLDR_BASE + "common/dtd/ldml.dtd";
176        LOCAL_SPPL_LDML_DTD = CLDR_BASE + "common/dtd/ldmlSupplemental.dtd";
177        SOURCE_FILE_DIR = CLDR_BASE + "common/main";
178        SPPL_SOURCE_FILE = CLDR_BASE + "common/supplemental/supplementalData.xml";
179        NUMBERING_SOURCE_FILE = CLDR_BASE + "common/supplemental/numberingSystems.xml";
180        METAZONES_SOURCE_FILE = CLDR_BASE + "common/supplemental/metaZones.xml";
181
182        bundleGenerator = new ResourceBundleGenerator();
183
184        List<Bundle> bundles = readBundleList();
185        convertBundles(bundles);
186    }
187
188    private static void usage() {
189        errout("Usage: java CLDRConverter [options]%n"
190                + "\t-help          output this usage message and exit%n"
191                + "\t-verbose       output information%n"
192                + "\t-draft [approved | provisional | unconfirmed]%n"
193                + "\t\t       draft level for using data (default: approved)%n"
194                + "\t-base dir      base directory for CLDR input files%n"
195                + "\t-o dir         output directory (defaut: ./build/gensrc)%n"
196                + "\t-utf8          use UTF-8 rather than \\uxxxx (for debug)%n");
197    }
198
199    static void info(String fmt, Object... args) {
200        if (verbose) {
201            System.out.printf(fmt, args);
202        }
203    }
204
205    static void info(String msg) {
206        if (verbose) {
207            System.out.println(msg);
208        }
209    }
210
211    static void warning(String fmt, Object... args) {
212        System.err.print("Warning: ");
213        System.err.printf(fmt, args);
214    }
215
216    static void warning(String msg) {
217        System.err.print("Warning: ");
218        errout(msg);
219    }
220
221    static void severe(String fmt, Object... args) {
222        System.err.print("Error: ");
223        System.err.printf(fmt, args);
224    }
225
226    static void severe(String msg) {
227        System.err.print("Error: ");
228        errout(msg);
229    }
230
231    private static void errout(String msg) {
232        if (msg.contains("%n")) {
233            System.err.printf(msg);
234        } else {
235            System.err.println(msg);
236        }
237    }
238
239    /**
240     * Configure the parser to allow access to DTDs on the file system.
241     */
242    private static void enableFileAccess(SAXParser parser) throws SAXNotSupportedException {
243        try {
244            parser.setProperty("http://javax.xml.XMLConstants/property/accessExternalDTD", "file");
245        } catch (SAXNotRecognizedException ignore) {
246            // property requires >= JAXP 1.5
247        }
248    }
249
250    private static List<Bundle> readBundleList() throws Exception {
251        ResourceBundle.Control defCon = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_DEFAULT);
252        List<Bundle> retList = new ArrayList<>();
253        Path path = FileSystems.getDefault().getPath(SOURCE_FILE_DIR);
254        try (DirectoryStream<Path> dirStr = Files.newDirectoryStream(path)) {
255            for (Path entry : dirStr) {
256                String fileName = entry.getFileName().toString();
257                if (fileName.endsWith(".xml")) {
258                    String id = fileName.substring(0, fileName.indexOf('.'));
259                    Locale cldrLoc = Locale.forLanguageTag(toLanguageTag(id));
260                    List<Locale> candList = defCon.getCandidateLocales("", cldrLoc);
261                    StringBuilder sb = new StringBuilder();
262                    for (Locale loc : candList) {
263                        if (!loc.equals(Locale.ROOT)) {
264                            sb.append(toLocaleName(loc.toLanguageTag()));
265                            sb.append(",");
266                        }
267                    }
268                    if (sb.indexOf("root") == -1) {
269                        sb.append("root");
270                    }
271                    Bundle b = new Bundle(id, sb.toString(), null, null);
272                    // Insert the bundle for en at the top so that it will get
273                    // processed first.
274                    if ("en".equals(id)) {
275                        retList.add(0, b);
276                    } else {
277                        retList.add(b);
278                    }
279                }
280            }
281        }
282        return retList;
283    }
284
285    private static Map<String, Map<String, Object>> cldrBundles = new HashMap<>();
286
287    static Map<String, Object> getCLDRBundle(String id) throws Exception {
288        Map<String, Object> bundle = cldrBundles.get(id);
289        if (bundle != null) {
290            return bundle;
291        }
292        SAXParserFactory factory = SAXParserFactory.newInstance();
293        factory.setValidating(true);
294        SAXParser parser = factory.newSAXParser();
295        enableFileAccess(parser);
296        LDMLParseHandler handler = new LDMLParseHandler(id);
297        File file = new File(SOURCE_FILE_DIR + File.separator + id + ".xml");
298        if (!file.exists()) {
299            // Skip if the file doesn't exist.
300            return Collections.emptyMap();
301        }
302
303        info("..... main directory .....");
304        info("Reading file " + file);
305        parser.parse(file, handler);
306
307        bundle = handler.getData();
308        cldrBundles.put(id, bundle);
309        String country = getCountryCode(id);
310        if (country != null) {
311            bundle = handlerSuppl.getData(country);
312            if (bundle != null) {
313                //merge two maps into one map
314                Map<String, Object> temp = cldrBundles.remove(id);
315                bundle.putAll(temp);
316                cldrBundles.put(id, bundle);
317            }
318        }
319        return bundle;
320    }
321
322    private static void convertBundles(List<Bundle> bundles) throws Exception {
323        // Parse SupplementalData file and store the information in the HashMap
324        // Calendar information such as firstDay and minDay are stored in
325        // supplementalData.xml as of CLDR1.4. Individual territory is listed
326        // with its ISO 3166 country code while default is listed using UNM49
327        // region and composition numerical code (001 for World.)
328        SAXParserFactory factorySuppl = SAXParserFactory.newInstance();
329        factorySuppl.setValidating(true);
330        SAXParser parserSuppl = factorySuppl.newSAXParser();
331        enableFileAccess(parserSuppl);
332        handlerSuppl = new SupplementDataParseHandler();
333        File fileSupply = new File(SPPL_SOURCE_FILE);
334        parserSuppl.parse(fileSupply, handlerSuppl);
335
336        // Parse numberingSystems to get digit zero character information.
337        SAXParserFactory numberingParser = SAXParserFactory.newInstance();
338        numberingParser.setValidating(true);
339        SAXParser parserNumbering = numberingParser.newSAXParser();
340        enableFileAccess(parserNumbering);
341        handlerNumbering = new NumberingSystemsParseHandler();
342        File fileNumbering = new File(NUMBERING_SOURCE_FILE);
343        parserNumbering.parse(fileNumbering, handlerNumbering);
344
345        // Parse metaZones to create mappings between Olson tzids and CLDR meta zone names
346        SAXParserFactory metazonesParser = SAXParserFactory.newInstance();
347        metazonesParser.setValidating(true);
348        SAXParser parserMetaZones = metazonesParser.newSAXParser();
349        enableFileAccess(parserMetaZones);
350        handlerMetaZones = new MetaZonesParseHandler();
351        File fileMetaZones = new File(METAZONES_SOURCE_FILE);
352        parserNumbering.parse(fileMetaZones, handlerMetaZones);
353
354        // For generating information on supported locales.
355        Map<String, SortedSet<String>> metaInfo = new HashMap<>();
356        metaInfo.put("LocaleNames", new TreeSet<String>());
357        metaInfo.put("CurrencyNames", new TreeSet<String>());
358        metaInfo.put("TimeZoneNames", new TreeSet<String>());
359        metaInfo.put("CalendarData", new TreeSet<String>());
360        metaInfo.put("FormatData", new TreeSet<String>());
361
362        for (Bundle bundle : bundles) {
363            // Get the target map, which contains all the data that should be
364            // visible for the bundle's locale
365
366            Map<String, Object> targetMap = bundle.getTargetMap();
367
368            EnumSet<Bundle.Type> bundleTypes = bundle.getBundleTypes();
369
370            // Fill in any missing resources in the base bundle from en and en-US data.
371            // This is because CLDR root.xml is supposed to be language neutral and doesn't
372            // provide some resource data. Currently, the runtime assumes that there are all
373            // resources though the parent resource bundle chain.
374            if (bundle.isRoot()) {
375                Map<String, Object> enData = new HashMap<>();
376                // Create a superset of en-US and en bundles data in order to
377                // fill in any missing resources in the base bundle.
378                enData.putAll(Bundle.getBundle("en").getTargetMap());
379                enData.putAll(Bundle.getBundle("en_US").getTargetMap());
380                for (String key : enData.keySet()) {
381                    if (!targetMap.containsKey(key)) {
382                        targetMap.put(key, enData.get(key));
383                    }
384                }
385                // Add DateTimePatternChars because CLDR no longer supports localized patterns.
386                targetMap.put("DateTimePatternChars", "GyMdkHmsSEDFwWahKzZ");
387            }
388
389            // Now the map contains just the entries that need to be in the resources bundles.
390            // Go ahead and generate them.
391            if (bundleTypes.contains(Bundle.Type.LOCALENAMES)) {
392                Map<String, Object> localeNamesMap = extractLocaleNames(targetMap, bundle.getID());
393                if (!localeNamesMap.isEmpty() || bundle.isRoot()) {
394                    metaInfo.get("LocaleNames").add(toLanguageTag(bundle.getID()));
395                    bundleGenerator.generateBundle("util", "LocaleNames", bundle.getID(), true, localeNamesMap, BundleType.OPEN);
396                }
397            }
398            if (bundleTypes.contains(Bundle.Type.CURRENCYNAMES)) {
399                Map<String, Object> currencyNamesMap = extractCurrencyNames(targetMap, bundle.getID(), bundle.getCurrencies());
400                if (!currencyNamesMap.isEmpty() || bundle.isRoot()) {
401                    metaInfo.get("CurrencyNames").add(toLanguageTag(bundle.getID()));
402                    bundleGenerator.generateBundle("util", "CurrencyNames", bundle.getID(), true, currencyNamesMap, BundleType.OPEN);
403                }
404            }
405            if (bundleTypes.contains(Bundle.Type.TIMEZONENAMES)) {
406                Map<String, Object> zoneNamesMap = extractZoneNames(targetMap, bundle.getID());
407                if (!zoneNamesMap.isEmpty() || bundle.isRoot()) {
408                    metaInfo.get("TimeZoneNames").add(toLanguageTag(bundle.getID()));
409                    bundleGenerator.generateBundle("util", "TimeZoneNames", bundle.getID(), true, zoneNamesMap, BundleType.TIMEZONE);
410                }
411            }
412            if (bundleTypes.contains(Bundle.Type.CALENDARDATA)) {
413                Map<String, Object> calendarDataMap = extractCalendarData(targetMap, bundle.getID());
414                if (!calendarDataMap.isEmpty() || bundle.isRoot()) {
415                    metaInfo.get("CalendarData").add(toLanguageTag(bundle.getID()));
416                    bundleGenerator.generateBundle("util", "CalendarData", bundle.getID(), true, calendarDataMap, BundleType.PLAIN);
417                }
418            }
419            if (bundleTypes.contains(Bundle.Type.FORMATDATA)) {
420                Map<String, Object> formatDataMap = extractFormatData(targetMap, bundle.getID());
421                // LocaleData.getAvailableLocales depends on having FormatData bundles around
422                if (!formatDataMap.isEmpty() || bundle.isRoot()) {
423                    metaInfo.get("FormatData").add(toLanguageTag(bundle.getID()));
424                    bundleGenerator.generateBundle("text", "FormatData", bundle.getID(), true, formatDataMap, BundleType.PLAIN);
425                }
426            }
427
428            // For testing
429            SortedSet<String> allLocales = new TreeSet<>();
430            allLocales.addAll(metaInfo.get("CurrencyNames"));
431            allLocales.addAll(metaInfo.get("LocaleNames"));
432            allLocales.addAll(metaInfo.get("CalendarData"));
433            allLocales.addAll(metaInfo.get("FormatData"));
434            metaInfo.put("All", allLocales);
435        }
436
437        bundleGenerator.generateMetaInfo(metaInfo);
438    }
439
440    /*
441     * Returns the language portion of the given id.
442     * If id is "root", "" is returned.
443     */
444    static String getLanguageCode(String id) {
445        int index = id.indexOf('_');
446        String lang = null;
447        if (index != -1) {
448            lang = id.substring(0, index);
449        } else {
450            lang = "root".equals(id) ? "" : id;
451        }
452        return lang;
453    }
454
455    /**
456     * Examine if the id includes the country (territory) code. If it does, it returns
457     * the country code.
458     * Otherwise, it returns null. eg. when the id is "zh_Hans_SG", it return "SG".
459     */
460    private static String getCountryCode(String id) {
461        //Truncate a variant code with '@' if there is any
462        //(eg. de_DE@collation=phonebook,currency=DOM)
463        if (id.indexOf('@') != -1) {
464            id = id.substring(0, id.indexOf('@'));
465        }
466        String[] tokens = id.split("_");
467        for (int index = 1; index < tokens.length; ++index) {
468            if (tokens[index].length() == 2
469                    && Character.isLetter(tokens[index].charAt(0))
470                    && Character.isLetter(tokens[index].charAt(1))) {
471                return tokens[index];
472            }
473        }
474        return null;
475    }
476
477    private static class KeyComparator implements Comparator<String> {
478        static KeyComparator INSTANCE = new KeyComparator();
479
480        private KeyComparator() {
481        }
482
483        @Override
484        public int compare(String o1, String o2) {
485            int len1 = o1.length();
486            int len2 = o2.length();
487            if (!isDigit(o1.charAt(0)) && !isDigit(o2.charAt(0))) {
488                // Shorter string comes first unless either starts with a digit.
489                if (len1 < len2) {
490                    return -1;
491                }
492                if (len1 > len2) {
493                    return 1;
494                }
495            }
496            return o1.compareTo(o2);
497        }
498
499        private boolean isDigit(char c) {
500            return c >= '0' && c <= '9';
501        }
502    }
503
504    private static Map<String, Object> extractLocaleNames(Map<String, Object> map, String id) {
505        Map<String, Object> localeNames = new TreeMap<>(KeyComparator.INSTANCE);
506        for (String key : map.keySet()) {
507            if (key.startsWith(LOCALE_NAME_PREFIX)) {
508                localeNames.put(key.substring(LOCALE_NAME_PREFIX.length()), map.get(key));
509            }
510        }
511        return localeNames;
512    }
513
514    @SuppressWarnings("AssignmentToForLoopParameter")
515    private static Map<String, Object> extractCurrencyNames(Map<String, Object> map, String id, String names)
516            throws Exception {
517        Map<String, Object> currencyNames = new TreeMap<>(KeyComparator.INSTANCE);
518        for (String key : map.keySet()) {
519            if (key.startsWith(CURRENCY_NAME_PREFIX)) {
520                currencyNames.put(key.substring(CURRENCY_NAME_PREFIX.length()), map.get(key));
521            } else if (key.startsWith(CURRENCY_SYMBOL_PREFIX)) {
522                currencyNames.put(key.substring(CURRENCY_SYMBOL_PREFIX.length()), map.get(key));
523            }
524        }
525        return currencyNames;
526    }
527
528    private static Map<String, Object> extractZoneNames(Map<String, Object> map, String id) {
529        Map<String, Object> names = new HashMap<>();
530        for (String tzid : handlerMetaZones.keySet()) {
531            String tzKey = TIMEZONE_ID_PREFIX + tzid;
532            Object data = map.get(tzKey);
533            if (data instanceof String[]) {
534                names.put(tzid, data);
535            } else {
536                String meta = handlerMetaZones.get(tzid);
537                if (meta != null) {
538                    String metaKey = METAZONE_ID_PREFIX + meta;
539                    data = map.get(metaKey);
540                    if (data instanceof String[]) {
541                        // Keep the metazone prefix here.
542                        names.put(metaKey, data);
543                        names.put(tzid, meta);
544                    }
545                }
546            }
547        }
548        return names;
549    }
550
551    private static Map<String, Object> extractCalendarData(Map<String, Object> map, String id) {
552        Map<String, Object> calendarData = new LinkedHashMap<>();
553        copyIfPresent(map, "firstDayOfWeek", calendarData);
554        copyIfPresent(map, "minimalDaysInFirstWeek", calendarData);
555        return calendarData;
556    }
557
558    static final String[] FORMAT_DATA_ELEMENTS = {
559        "MonthNames",
560        "standalone.MonthNames",
561        "MonthAbbreviations",
562        "standalone.MonthAbbreviations",
563        "MonthNarrows",
564        "standalone.MonthNarrows",
565        "DayNames",
566        "standalone.DayNames",
567        "DayAbbreviations",
568        "standalone.DayAbbreviations",
569        "DayNarrows",
570        "standalone.DayNarrows",
571        "QuarterNames",
572        "standalone.QuarterNames",
573        "QuarterAbbreviations",
574        "standalone.QuarterAbbreviations",
575        "QuarterNarrows",
576        "standalone.QuarterNarrows",
577        "AmPmMarkers",
578        "narrow.AmPmMarkers",
579        "long.Eras",
580        "Eras",
581        "narrow.Eras",
582        "field.era",
583        "field.year",
584        "field.month",
585        "field.week",
586        "field.weekday",
587        "field.dayperiod",
588        "field.hour",
589        "field.minute",
590        "field.second",
591        "field.zone",
592        "TimePatterns",
593        "DatePatterns",
594        "DateTimePatterns",
595        "DateTimePatternChars"
596    };
597
598    private static Map<String, Object> extractFormatData(Map<String, Object> map, String id) {
599        Map<String, Object> formatData = new LinkedHashMap<>();
600        for (CalendarType calendarType : CalendarType.values()) {
601            String prefix = calendarType.keyElementName();
602            for (String element : FORMAT_DATA_ELEMENTS) {
603                String key = prefix + element;
604                copyIfPresent(map, "java.time." + key, formatData);
605                copyIfPresent(map, key, formatData);
606            }
607        }
608        // Workaround for islamic-umalqura name support (JDK-8015986)
609        switch (id) {
610        case "ar":
611            map.put(CLDRConverter.CALENDAR_NAME_PREFIX
612                    + CalendarType.ISLAMIC_UMALQURA.lname(),
613                    // derived from CLDR 24 draft
614                    "\u0627\u0644\u062a\u0642\u0648\u064a\u0645 "
615                    +"\u0627\u0644\u0625\u0633\u0644\u0627\u0645\u064a "
616                    +"[\u0623\u0645 \u0627\u0644\u0642\u0631\u0649]");
617            break;
618        case "en":
619            map.put(CLDRConverter.CALENDAR_NAME_PREFIX
620                    + CalendarType.ISLAMIC_UMALQURA.lname(),
621                    // derived from CLDR 24 draft
622                    "Islamic Calendar [Umm al-Qura]");
623            break;
624        }
625        // Copy available calendar names
626        for (String key : map.keySet()) {
627            if (key.startsWith(CLDRConverter.CALENDAR_NAME_PREFIX)) {
628                String type = key.substring(CLDRConverter.CALENDAR_NAME_PREFIX.length());
629                for (CalendarType calendarType : CalendarType.values()) {
630                    if (type.equals(calendarType.lname())) {
631                        Object value = map.get(key);
632                        formatData.put(key, value);
633                        String ukey = CLDRConverter.CALENDAR_NAME_PREFIX + calendarType.uname();
634                        if (!key.equals(ukey)) {
635                            formatData.put(ukey, value);
636                        }
637                    }
638                }
639            }
640        }
641
642        copyIfPresent(map, "DefaultNumberingSystem", formatData);
643
644        @SuppressWarnings("unchecked")
645        List<String> numberingScripts = (List<String>) map.remove("numberingScripts");
646        if (numberingScripts != null) {
647            for (String script : numberingScripts) {
648                copyIfPresent(map, script + "." + "NumberElements", formatData);
649            }
650        } else {
651            copyIfPresent(map, "NumberElements", formatData);
652        }
653        copyIfPresent(map, "NumberPatterns", formatData);
654        return formatData;
655    }
656
657    private static void copyIfPresent(Map<String, Object> src, String key, Map<String, Object> dest) {
658        Object value = src.get(key);
659        if (value != null) {
660            dest.put(key, value);
661        }
662    }
663
664    // --- code below here is adapted from java.util.Properties ---
665    private static final String specialSaveCharsJava = "\"";
666    private static final String specialSaveCharsProperties = "=: \t\r\n\f#!";
667
668    /*
669     * Converts unicodes to encoded &#92;uxxxx
670     * and writes out any of the characters in specialSaveChars
671     * with a preceding slash
672     */
673    static String saveConvert(String theString, boolean useJava) {
674        if (theString == null) {
675            return "";
676        }
677
678        String specialSaveChars;
679        if (useJava) {
680            specialSaveChars = specialSaveCharsJava;
681        } else {
682            specialSaveChars = specialSaveCharsProperties;
683        }
684        boolean escapeSpace = false;
685
686        int len = theString.length();
687        StringBuilder outBuffer = new StringBuilder(len * 2);
688        Formatter formatter = new Formatter(outBuffer, Locale.ROOT);
689
690        for (int x = 0; x < len; x++) {
691            char aChar = theString.charAt(x);
692            switch (aChar) {
693            case ' ':
694                if (x == 0 || escapeSpace) {
695                    outBuffer.append('\\');
696                }
697                outBuffer.append(' ');
698                break;
699            case '\\':
700                outBuffer.append('\\');
701                outBuffer.append('\\');
702                break;
703            case '\t':
704                outBuffer.append('\\');
705                outBuffer.append('t');
706                break;
707            case '\n':
708                outBuffer.append('\\');
709                outBuffer.append('n');
710                break;
711            case '\r':
712                outBuffer.append('\\');
713                outBuffer.append('r');
714                break;
715            case '\f':
716                outBuffer.append('\\');
717                outBuffer.append('f');
718                break;
719            default:
720                if (aChar < 0x0020 || (!USE_UTF8 && aChar > 0x007e)) {
721                    formatter.format("\\u%04x", (int)aChar);
722                } else {
723                    if (specialSaveChars.indexOf(aChar) != -1) {
724                        outBuffer.append('\\');
725                    }
726                    outBuffer.append(aChar);
727                }
728            }
729        }
730        return outBuffer.toString();
731    }
732
733    private static String toLanguageTag(String locName) {
734        if (locName.indexOf('_') == -1) {
735            return locName;
736        }
737        String tag = locName.replaceAll("_", "-");
738        Locale loc = Locale.forLanguageTag(tag);
739        return loc.toLanguageTag();
740    }
741
742    private static String toLocaleName(String tag) {
743        if (tag.indexOf('-') == -1) {
744            return tag;
745        }
746        return tag.replaceAll("-", "_");
747    }
748}
749