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