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