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