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 
 410         // parent locales map. The mappings are put in base metaInfo file
 411         // for now.
 412         if (isBaseModule) {
 413             metaInfo.putAll(parentLocalesMap);
 414         }
 415 
 416         for (Bundle bundle : bundles) {
 417             // Get the target map, which contains all the data that should be
 418             // visible for the bundle's locale
 419 
 420             Map<String, Object> targetMap = bundle.getTargetMap();
 421 
 422             EnumSet<Bundle.Type> bundleTypes = bundle.getBundleTypes();
 423 
 424             if (bundle.isRoot()) {
 425                 // Add DateTimePatternChars because CLDR no longer supports localized patterns.
 426                 targetMap.put("DateTimePatternChars", "GyMdkHmsSEDFwWahKzZ");
 427             }
 428 
 429             // Now the map contains just the entries that need to be in the resources bundles.
 430             // Go ahead and generate them.
 431             if (bundleTypes.contains(Bundle.Type.LOCALENAMES)) {
 432                 Map<String, Object> localeNamesMap = extractLocaleNames(targetMap, bundle.getID());
 433                 if (!localeNamesMap.isEmpty() || bundle.isRoot()) {
 434                     metaInfo.get("LocaleNames").add(toLanguageTag(bundle.getID()));
 435                     bundleGenerator.generateBundle("util", "LocaleNames", bundle.getID(), true, localeNamesMap, BundleType.OPEN);
 436                 }
 437             }
 438             if (bundleTypes.contains(Bundle.Type.CURRENCYNAMES)) {
 439                 Map<String, Object> currencyNamesMap = extractCurrencyNames(targetMap, bundle.getID(), bundle.getCurrencies());
 440                 if (!currencyNamesMap.isEmpty() || bundle.isRoot()) {
 441                     metaInfo.get("CurrencyNames").add(toLanguageTag(bundle.getID()));
 442                     bundleGenerator.generateBundle("util", "CurrencyNames", bundle.getID(), true, currencyNamesMap, BundleType.OPEN);
 443                 }
 444             }
 445             if (bundleTypes.contains(Bundle.Type.TIMEZONENAMES)) {
 446                 Map<String, Object> zoneNamesMap = extractZoneNames(targetMap, bundle.getID());
 447                 if (!zoneNamesMap.isEmpty() || bundle.isRoot()) {
 448                     metaInfo.get("TimeZoneNames").add(toLanguageTag(bundle.getID()));
 449                     bundleGenerator.generateBundle("util", "TimeZoneNames", bundle.getID(), true, zoneNamesMap, BundleType.TIMEZONE);
 450                 }
 451             }
 452             if (bundleTypes.contains(Bundle.Type.CALENDARDATA)) {
 453                 Map<String, Object> calendarDataMap = extractCalendarData(targetMap, bundle.getID());
 454                 if (!calendarDataMap.isEmpty() || bundle.isRoot()) {
 455                     metaInfo.get("CalendarData").add(toLanguageTag(bundle.getID()));
 456                     bundleGenerator.generateBundle("util", "CalendarData", bundle.getID(), true, calendarDataMap, BundleType.PLAIN);
 457                 }
 458             }
 459             if (bundleTypes.contains(Bundle.Type.FORMATDATA)) {
 460                 Map<String, Object> formatDataMap = extractFormatData(targetMap, bundle.getID());
 461                 // LocaleData.getAvailableLocales depends on having FormatData bundles around
 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 testing
 469             SortedSet<String> allLocales = new TreeSet<>();
 470             allLocales.addAll(metaInfo.get("CurrencyNames"));
 471             allLocales.addAll(metaInfo.get("LocaleNames"));
 472             allLocales.addAll(metaInfo.get("CalendarData"));
 473             allLocales.addAll(metaInfo.get("FormatData"));
 474             metaInfo.put("AvailableLocales", allLocales);
 475         }
 476 
 477         bundleGenerator.generateMetaInfo(metaInfo);
 478     }
 479 
 480     static final Map<String, String> aliases = new HashMap<>();
 481 
 482     /**
 483      * Translate the aliases into the real entries in the bundle map.
 484      */
 485     static void handleAliases(Map<String, Object> bundleMap) {
 486         Set bundleKeys = bundleMap.keySet();
 487         try {
 488             for (String key : aliases.keySet()) {
 489                 String targetKey = aliases.get(key);
 490                 if (bundleKeys.contains(targetKey)) {
 491                     bundleMap.putIfAbsent(key, bundleMap.get(targetKey));
 492                 }
 493             }
 494         } catch (Exception ex) {
 495             Logger.getLogger(CLDRConverter.class.getName()).log(Level.SEVERE, null, ex);
 496         }
 497     }
 498 
 499     /*
 500      * Returns the language portion of the given id.
 501      * If id is "root", "" is returned.
 502      */
 503     static String getLanguageCode(String id) {
 504         return "root".equals(id) ? "" : Locale.forLanguageTag(id.replaceAll("_", "-")).getLanguage();
 505     }
 506 
 507     /**
 508      * Examine if the id includes the country (territory) code. If it does, it returns
 509      * the country code.
 510      * Otherwise, it returns null. eg. when the id is "zh_Hans_SG", it return "SG".
 511      * For now, it does not return US M.49 code, e.g., '001', as those three digit numbers cannot
 512      * be translated into package names.
 513      */
 514     static String getCountryCode(String id) {
 515         String ctry = Locale.forLanguageTag(id.replaceAll("_", "-")).getCountry();
 516         return ctry.length() == 2 ? ctry : null;
 517     }
 518 
 519     private static class KeyComparator implements Comparator<String> {
 520         static KeyComparator INSTANCE = new KeyComparator();
 521 
 522         private KeyComparator() {
 523         }
 524 
 525         @Override
 526         public int compare(String o1, String o2) {
 527             int len1 = o1.length();
 528             int len2 = o2.length();
 529             if (!isDigit(o1.charAt(0)) && !isDigit(o2.charAt(0))) {
 530                 // Shorter string comes first unless either starts with a digit.
 531                 if (len1 < len2) {
 532                     return -1;
 533                 }
 534                 if (len1 > len2) {
 535                     return 1;
 536                 }
 537             }
 538             return o1.compareTo(o2);
 539         }
 540 
 541         private boolean isDigit(char c) {
 542             return c >= '0' && c <= '9';
 543         }
 544     }
 545 
 546     private static Map<String, Object> extractLocaleNames(Map<String, Object> map, String id) {
 547         Map<String, Object> localeNames = new TreeMap<>(KeyComparator.INSTANCE);
 548         for (String key : map.keySet()) {
 549             if (key.startsWith(LOCALE_NAME_PREFIX)) {
 550                 localeNames.put(key.substring(LOCALE_NAME_PREFIX.length()), map.get(key));
 551             }
 552         }
 553         return localeNames;
 554     }
 555 
 556     @SuppressWarnings("AssignmentToForLoopParameter")
 557     private static Map<String, Object> extractCurrencyNames(Map<String, Object> map, String id, String names)
 558             throws Exception {
 559         Map<String, Object> currencyNames = new TreeMap<>(KeyComparator.INSTANCE);
 560         for (String key : map.keySet()) {
 561             if (key.startsWith(CURRENCY_NAME_PREFIX)) {
 562                 currencyNames.put(key.substring(CURRENCY_NAME_PREFIX.length()), map.get(key));
 563             } else if (key.startsWith(CURRENCY_SYMBOL_PREFIX)) {
 564                 currencyNames.put(key.substring(CURRENCY_SYMBOL_PREFIX.length()), map.get(key));
 565             }
 566         }
 567         return currencyNames;
 568     }
 569 
 570     private static Map<String, Object> extractZoneNames(Map<String, Object> map, String id) {
 571         Map<String, Object> names = new HashMap<>();
 572         for (String tzid : handlerMetaZones.keySet()) {
 573             String tzKey = TIMEZONE_ID_PREFIX + tzid;
 574             Object data = map.get(tzKey);
 575             if (data instanceof String[]) {
 576                 names.put(tzid, data);
 577             } else {
 578                 String meta = handlerMetaZones.get(tzid);
 579                 if (meta != null) {
 580                     String metaKey = METAZONE_ID_PREFIX + meta;
 581                     data = map.get(metaKey);
 582                     if (data instanceof String[]) {
 583                         // Keep the metazone prefix here.
 584                         names.put(metaKey, data);
 585                         names.put(tzid, meta);
 586                     }
 587                 }
 588             }
 589         }
 590         return names;
 591     }
 592 
 593     private static Map<String, Object> extractCalendarData(Map<String, Object> map, String id) {
 594         Map<String, Object> calendarData = new LinkedHashMap<>();
 595         copyIfPresent(map, "firstDayOfWeek", calendarData);
 596         copyIfPresent(map, "minimalDaysInFirstWeek", calendarData);
 597         return calendarData;
 598     }
 599 
 600     static final String[] FORMAT_DATA_ELEMENTS = {
 601         "MonthNames",
 602         "standalone.MonthNames",
 603         "MonthAbbreviations",
 604         "standalone.MonthAbbreviations",
 605         "MonthNarrows",
 606         "standalone.MonthNarrows",
 607         "DayNames",
 608         "standalone.DayNames",
 609         "DayAbbreviations",
 610         "standalone.DayAbbreviations",
 611         "DayNarrows",
 612         "standalone.DayNarrows",
 613         "QuarterNames",
 614         "standalone.QuarterNames",
 615         "QuarterAbbreviations",
 616         "standalone.QuarterAbbreviations",
 617         "QuarterNarrows",
 618         "standalone.QuarterNarrows",
 619         "AmPmMarkers",
 620         "narrow.AmPmMarkers",
 621         "long.Eras",
 622         "Eras",
 623         "narrow.Eras",
 624         "field.era",
 625         "field.year",
 626         "field.month",
 627         "field.week",
 628         "field.weekday",
 629         "field.dayperiod",
 630         "field.hour",
 631         "field.minute",
 632         "field.second",
 633         "field.zone",
 634         "TimePatterns",
 635         "DatePatterns",
 636         "DateTimePatterns",
 637         "DateTimePatternChars"
 638     };
 639 
 640     private static Map<String, Object> extractFormatData(Map<String, Object> map, String id) {
 641         Map<String, Object> formatData = new LinkedHashMap<>();
 642         for (CalendarType calendarType : CalendarType.values()) {
 643             if (calendarType == CalendarType.GENERIC) {
 644                 continue;
 645             }
 646             String prefix = calendarType.keyElementName();
 647             for (String element : FORMAT_DATA_ELEMENTS) {
 648                 String key = prefix + element;
 649                 copyIfPresent(map, "java.time." + key, formatData);
 650                 copyIfPresent(map, key, formatData);
 651             }
 652         }
 653 
 654         for (String key : map.keySet()) {
 655             // Copy available calendar names
 656             if (key.startsWith(CLDRConverter.CALENDAR_NAME_PREFIX)) {
 657                 String type = key.substring(CLDRConverter.CALENDAR_NAME_PREFIX.length());
 658                 for (CalendarType calendarType : CalendarType.values()) {
 659                     if (calendarType == CalendarType.GENERIC) {
 660                         continue;
 661                     }
 662                     if (type.equals(calendarType.lname())) {
 663                         Object value = map.get(key);
 664                         formatData.put(key, value);
 665                         String ukey = CLDRConverter.CALENDAR_NAME_PREFIX + calendarType.uname();
 666                         if (!key.equals(ukey)) {
 667                             formatData.put(ukey, value);
 668                         }
 669                     }
 670                 }
 671             }
 672         }
 673 
 674         copyIfPresent(map, "DefaultNumberingSystem", formatData);
 675 
 676         @SuppressWarnings("unchecked")
 677         List<String> numberingScripts = (List<String>) map.remove("numberingScripts");
 678         if (numberingScripts != null) {
 679             for (String script : numberingScripts) {
 680                 copyIfPresent(map, script + "." + "NumberElements", formatData);
 681             }
 682         } else {
 683             copyIfPresent(map, "NumberElements", formatData);
 684         }
 685         copyIfPresent(map, "NumberPatterns", formatData);
 686         return formatData;
 687     }
 688 
 689     private static void copyIfPresent(Map<String, Object> src, String key, Map<String, Object> dest) {
 690         Object value = src.get(key);
 691         if (value != null) {
 692             dest.put(key, value);
 693         }
 694     }
 695 
 696     // --- code below here is adapted from java.util.Properties ---
 697     private static final String specialSaveCharsJava = "\"";
 698     private static final String specialSaveCharsProperties = "=: \t\r\n\f#!";
 699 
 700     /*
 701      * Converts unicodes to encoded &#92;uxxxx
 702      * and writes out any of the characters in specialSaveChars
 703      * with a preceding slash
 704      */
 705     static String saveConvert(String theString, boolean useJava) {
 706         if (theString == null) {
 707             return "";
 708         }
 709 
 710         String specialSaveChars;
 711         if (useJava) {
 712             specialSaveChars = specialSaveCharsJava;
 713         } else {
 714             specialSaveChars = specialSaveCharsProperties;
 715         }
 716         boolean escapeSpace = false;
 717 
 718         int len = theString.length();
 719         StringBuilder outBuffer = new StringBuilder(len * 2);
 720         Formatter formatter = new Formatter(outBuffer, Locale.ROOT);
 721 
 722         for (int x = 0; x < len; x++) {
 723             char aChar = theString.charAt(x);
 724             switch (aChar) {
 725             case ' ':
 726                 if (x == 0 || escapeSpace) {
 727                     outBuffer.append('\\');
 728                 }
 729                 outBuffer.append(' ');
 730                 break;
 731             case '\\':
 732                 outBuffer.append('\\');
 733                 outBuffer.append('\\');
 734                 break;
 735             case '\t':
 736                 outBuffer.append('\\');
 737                 outBuffer.append('t');
 738                 break;
 739             case '\n':
 740                 outBuffer.append('\\');
 741                 outBuffer.append('n');
 742                 break;
 743             case '\r':
 744                 outBuffer.append('\\');
 745                 outBuffer.append('r');
 746                 break;
 747             case '\f':
 748                 outBuffer.append('\\');
 749                 outBuffer.append('f');
 750                 break;
 751             default:
 752                 if (aChar < 0x0020 || (!USE_UTF8 && aChar > 0x007e)) {
 753                     formatter.format("\\u%04x", (int)aChar);
 754                 } else {
 755                     if (specialSaveChars.indexOf(aChar) != -1) {
 756                         outBuffer.append('\\');
 757                     }
 758                     outBuffer.append(aChar);
 759                 }
 760             }
 761         }
 762         return outBuffer.toString();
 763     }
 764 
 765     private static String toLanguageTag(String locName) {
 766         if (locName.indexOf('_') == -1) {
 767             return locName;
 768         }
 769         String tag = locName.replaceAll("_", "-");
 770         Locale loc = Locale.forLanguageTag(tag);
 771         return loc.toLanguageTag();
 772     }
 773 
 774     private static String toLocaleName(String tag) {
 775         if (tag.indexOf('-') == -1) {
 776             return tag;
 777         }
 778         return tag.replaceAll("-", "_");
 779     }
 780 
 781     private static void setupBaseLocales(String localeList) {
 782         Arrays.stream(localeList.split(","))
 783             .map(Locale::forLanguageTag)
 784             .map(l -> Control.getControl(Control.FORMAT_DEFAULT)
 785                              .getCandidateLocales("", l))
 786             .forEach(BASE_LOCALES::addAll);
 787     }
 788 
 789     // applying parent locale rules to the passed candidates list
 790     // This has to match with the one in sun.util.cldr.CLDRLocaleProviderAdapter
 791     private static Map<Locale, Locale> childToParentLocaleMap = null;
 792     private static List<Locale> applyParentLocales(String baseName, List<Locale> candidates) {
 793         if (Objects.isNull(childToParentLocaleMap)) {
 794             childToParentLocaleMap = new HashMap<>();
 795             parentLocalesMap.keySet().forEach(key -> {
 796                 String parent = key.substring(PARENT_LOCALE_PREFIX.length()).replaceAll("_", "-");
 797                 parentLocalesMap.get(key).stream().forEach(child -> {
 798                     childToParentLocaleMap.put(Locale.forLanguageTag(child),
 799                         "root".equals(parent) ? Locale.ROOT : Locale.forLanguageTag(parent));
 800                 });
 801             });
 802         }
 803 
 804         // check irregular parents
 805         for (int i = 0; i < candidates.size(); i++) {
 806             Locale l = candidates.get(i);
 807             Locale p = childToParentLocaleMap.get(l);
 808             if (!l.equals(Locale.ROOT) &&
 809                 Objects.nonNull(p) &&
 810                 !candidates.get(i+1).equals(p)) {
 811                 List<Locale> applied = candidates.subList(0, i+1);
 812                 applied.addAll(applyParentLocales(baseName, defCon.getCandidateLocales(baseName, p)));
 813                 return applied;
 814             }
 815         }
 816 
 817         return candidates;
 818     }
 819 }