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