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