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 import org.xml.sax.SAXNotRecognizedException;
  38 import 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  */
  46 public 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("AvailableLocales", 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 }