1 /*
   2  * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package build.tools.cldrconverter;
  27 
  28 import build.tools.cldrconverter.BundleGenerator.BundleType;
  29 import java.io.File;
  30 import java.nio.file.DirectoryStream;
  31 import java.nio.file.FileSystems;
  32 import java.nio.file.Files;
  33 import java.nio.file.Path;
  34 import java.util.*;
  35 import javax.xml.parsers.SAXParser;
  36 import javax.xml.parsers.SAXParserFactory;
  37 
  38 
  39 /**
  40  * Converts locale data from "Locale Data Markup Language" format to
  41  * JRE resource bundle format. LDML is the format used by the Common
  42  * Locale Data Repository maintained by the Unicode Consortium.
  43  */
  44 public class CLDRConverter {
  45 
  46     static final String LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldml.dtd";
  47     static final String SPPL_LDML_DTD_SYSTEM_ID = "http://www.unicode.org/cldr/dtd/2.0/ldmlSupplemental.dtd";
  48 
  49     private static String CLDR_BASE = "../CLDR/21.0.1/";
  50     static String LOCAL_LDML_DTD;
  51     static String LOCAL_SPPL_LDML_DTD;
  52     private static String SOURCE_FILE_DIR;
  53     private static String SPPL_SOURCE_FILE;
  54     private static String NUMBERING_SOURCE_FILE;
  55     private static String METAZONES_SOURCE_FILE;
  56     static String DESTINATION_DIR = "build/gensrc";
  57 
  58     static final String LOCALE_NAME_PREFIX = "locale.displayname.";
  59     static final String CURRENCY_SYMBOL_PREFIX = "currency.symbol.";
  60     static final String CURRENCY_NAME_PREFIX = "currency.displayname.";
  61     static final String CALENDAR_NAME_PREFIX = "calendarname.";
  62     static final String TIMEZONE_ID_PREFIX = "timezone.id.";
  63     static final String ZONE_NAME_PREFIX = "timezone.displayname.";
  64     static final String METAZONE_ID_PREFIX = "metazone.id.";
  65 
  66     private static SupplementDataParseHandler handlerSuppl;
  67     static NumberingSystemsParseHandler handlerNumbering;
  68     static MetaZonesParseHandler handlerMetaZones;
  69     private static BundleGenerator bundleGenerator;
  70 
  71     static enum DraftType {
  72         UNCONFIRMED,
  73         PROVISIONAL,
  74         CONTRIBUTED,
  75         APPROVED;
  76 
  77         private static final Map<String, DraftType> map = new HashMap<>();
  78         static {
  79             for (DraftType dt : values()) {
  80                 map.put(dt.getKeyword(), dt);
  81             }
  82         }
  83         static private DraftType defaultType = CONTRIBUTED;
  84 
  85         private final String keyword;
  86 
  87         private DraftType() {
  88             keyword = this.name().toLowerCase(Locale.ROOT);
  89 
  90         }
  91 
  92         static DraftType forKeyword(String keyword) {
  93             return map.get(keyword);
  94         }
  95 
  96         static DraftType getDefault() {
  97             return defaultType;
  98         }
  99 
 100         static void setDefault(String keyword) {
 101             defaultType = Objects.requireNonNull(forKeyword(keyword));
 102         }
 103 
 104         String getKeyword() {
 105             return keyword;
 106         }
 107     }
 108 
 109     static boolean USE_UTF8 = false;
 110     private static boolean verbose;
 111 
 112     private CLDRConverter() {
 113        // no instantiation
 114     }
 115 
 116     @SuppressWarnings("AssignmentToForLoopParameter")
 117     public static void main(String[] args) throws Exception {
 118         if (args.length != 0) {
 119             String currentArg = null;
 120             try {
 121                 for (int i = 0; i < args.length; i++) {
 122                     currentArg = args[i];
 123                     switch (currentArg) {
 124                     case "-draft":
 125                         String draftDataType = args[++i];
 126                         try {
 127                             DraftType.setDefault(draftDataType);
 128                         } catch (NullPointerException e) {
 129                             severe("Error: incorrect draft value: %s%n", draftDataType);
 130                             System.exit(1);
 131                         }
 132                         info("Using the specified data type: %s%n", draftDataType);
 133                         break;
 134 
 135                     case "-base":
 136                         // base directory for input files
 137                         CLDR_BASE = args[++i];
 138                         if (!CLDR_BASE.endsWith("/")) {
 139                             CLDR_BASE += "/";
 140                         }
 141                         break;
 142 
 143                     case "-o":
 144                         // output directory
 145                         DESTINATION_DIR = args[++i];
 146                         break;
 147 
 148                     case "-utf8":
 149                         USE_UTF8 = true;
 150                         break;
 151 
 152                     case "-verbose":
 153                         verbose = true;
 154                         break;
 155 
 156                     case "-help":
 157                         usage();
 158                         System.exit(0);
 159                         break;
 160 
 161                     default:
 162                         throw new RuntimeException();
 163                     }
 164                 }
 165             } catch (RuntimeException e) {
 166                 severe("unknown or imcomplete arg(s): " + currentArg);
 167                 usage();
 168                 System.exit(1);
 169             }
 170         }
 171 
 172         // Set up path names
 173         LOCAL_LDML_DTD = CLDR_BASE + "common/dtd/ldml.dtd";
 174         LOCAL_SPPL_LDML_DTD = CLDR_BASE + "common/dtd/ldmlSupplemental.dtd";
 175         SOURCE_FILE_DIR = CLDR_BASE + "common/main";
 176         SPPL_SOURCE_FILE = CLDR_BASE + "common/supplemental/supplementalData.xml";
 177         NUMBERING_SOURCE_FILE = CLDR_BASE + "common/supplemental/numberingSystems.xml";
 178         METAZONES_SOURCE_FILE = CLDR_BASE + "common/supplemental/metaZones.xml";
 179 
 180         bundleGenerator = new ResourceBundleGenerator();
 181 
 182         List<Bundle> bundles = readBundleList();
 183         convertBundles(bundles);
 184     }
 185 
 186     private static void usage() {
 187         errout("Usage: java CLDRConverter [options]%n"
 188                 + "\t-help          output this usage message and exit%n"
 189                 + "\t-verbose       output information%n"
 190                 + "\t-draft [approved | provisional | unconfirmed]%n"
 191                 + "\t\t       draft level for using data (default: approved)%n"
 192                 + "\t-base dir      base directory for CLDR input files%n"
 193                 + "\t-o dir         output directory (defaut: ./build/gensrc)%n"
 194                 + "\t-utf8          use UTF-8 rather than \\uxxxx (for debug)%n");
 195     }
 196 
 197     static void info(String fmt, Object... args) {
 198         if (verbose) {
 199             System.out.printf(fmt, args);
 200         }
 201     }
 202 
 203     static void info(String msg) {
 204         if (verbose) {
 205             System.out.println(msg);
 206         }
 207     }
 208 
 209     static void warning(String fmt, Object... args) {
 210         System.err.print("Warning: ");
 211         System.err.printf(fmt, args);
 212     }
 213 
 214     static void warning(String msg) {
 215         System.err.print("Warning: ");
 216         errout(msg);
 217     }
 218 
 219     static void severe(String fmt, Object... args) {
 220         System.err.print("Error: ");
 221         System.err.printf(fmt, args);
 222     }
 223 
 224     static void severe(String msg) {
 225         System.err.print("Error: ");
 226         errout(msg);
 227     }
 228 
 229     private static void errout(String msg) {
 230         if (msg.contains("%n")) {
 231             System.err.printf(msg);
 232         } else {
 233             System.err.println(msg);
 234         }
 235     }
 236 
 237     private static List<Bundle> readBundleList() throws Exception {
 238         ResourceBundle.Control defCon = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_DEFAULT);
 239         List<Bundle> retList = new ArrayList<>();
 240         Path path = FileSystems.getDefault().getPath(SOURCE_FILE_DIR);
 241         try (DirectoryStream<Path> dirStr = Files.newDirectoryStream(path)) {
 242             for (Path entry : dirStr) {
 243                 String fileName = entry.getFileName().toString();
 244                 if (fileName.endsWith(".xml")) {
 245                     String id = fileName.substring(0, fileName.indexOf('.'));
 246                     Locale cldrLoc = Locale.forLanguageTag(toLanguageTag(id));
 247                     List<Locale> candList = defCon.getCandidateLocales("", cldrLoc);
 248                     StringBuilder sb = new StringBuilder();
 249                     for (Locale loc : candList) {
 250                         if (!loc.equals(Locale.ROOT)) {
 251                             sb.append(toLocaleName(loc.toLanguageTag()));
 252                             sb.append(",");
 253                         }
 254                     }
 255                     if (sb.indexOf("root") == -1) {
 256                         sb.append("root");
 257                     }
 258                     Bundle b = new Bundle(id, sb.toString(), null, null);
 259                     // Insert the bundle for en at the top so that it will get
 260                     // processed first.
 261                     if ("en".equals(id)) {
 262                         retList.add(0, b);
 263                     } else {
 264                         retList.add(b);
 265                     }
 266                 }
 267             }
 268         }
 269         return retList;
 270     }
 271 
 272     private static Map<String, Map<String, Object>> cldrBundles = new HashMap<>();
 273 
 274     static Map<String, Object> getCLDRBundle(String id) throws Exception {
 275         Map<String, Object> bundle = cldrBundles.get(id);
 276         if (bundle != null) {
 277             return bundle;
 278         }
 279         SAXParserFactory factory = SAXParserFactory.newInstance();
 280         factory.setValidating(true);
 281         SAXParser parser = factory.newSAXParser();
 282         LDMLParseHandler handler = new LDMLParseHandler(id);
 283         File file = new File(SOURCE_FILE_DIR + File.separator + id + ".xml");
 284         if (!file.exists()) {
 285             // Skip if the file doesn't exist.
 286             return Collections.emptyMap();
 287         }
 288 
 289         info("..... main directory .....");
 290         info("Reading file " + file);
 291         parser.parse(file, handler);
 292 
 293         bundle = handler.getData();
 294         cldrBundles.put(id, bundle);
 295         String country = getCountryCode(id);
 296         if (country != null) {
 297             bundle = handlerSuppl.getData(country);
 298             if (bundle != null) {
 299                 //merge two maps into one map
 300                 Map<String, Object> temp = cldrBundles.remove(id);
 301                 bundle.putAll(temp);
 302                 cldrBundles.put(id, bundle);
 303             }
 304         }
 305         return bundle;
 306     }
 307 
 308     private static void convertBundles(List<Bundle> bundles) throws Exception {
 309         // Parse SupplementalData file and store the information in the HashMap
 310         // Calendar information such as firstDay and minDay are stored in
 311         // supplementalData.xml as of CLDR1.4. Individual territory is listed
 312         // with its ISO 3166 country code while default is listed using UNM49
 313         // region and composition numerical code (001 for World.)
 314         SAXParserFactory factorySuppl = SAXParserFactory.newInstance();
 315         factorySuppl.setValidating(true);
 316         SAXParser parserSuppl = factorySuppl.newSAXParser();
 317         handlerSuppl = new SupplementDataParseHandler();
 318         File fileSupply = new File(SPPL_SOURCE_FILE);
 319         parserSuppl.parse(fileSupply, handlerSuppl);
 320 
 321         // Parse numberingSystems to get digit zero character information.
 322         SAXParserFactory numberingParser = SAXParserFactory.newInstance();
 323         numberingParser.setValidating(true);
 324         SAXParser parserNumbering = numberingParser.newSAXParser();
 325         handlerNumbering = new NumberingSystemsParseHandler();
 326         File fileNumbering = new File(NUMBERING_SOURCE_FILE);
 327         parserNumbering.parse(fileNumbering, handlerNumbering);
 328 
 329         // Parse metaZones to create mappings between Olson tzids and CLDR meta zone names
 330         SAXParserFactory metazonesParser = SAXParserFactory.newInstance();
 331         metazonesParser.setValidating(true);
 332         SAXParser parserMetaZones = metazonesParser.newSAXParser();
 333         handlerMetaZones = new MetaZonesParseHandler();
 334         File fileMetaZones = new File(METAZONES_SOURCE_FILE);
 335         parserNumbering.parse(fileMetaZones, handlerMetaZones);
 336 
 337         // For generating information on supported locales.
 338         Map<String, SortedSet<String>> metaInfo = new HashMap<>();
 339         metaInfo.put("LocaleNames", new TreeSet<String>());
 340         metaInfo.put("CurrencyNames", new TreeSet<String>());
 341         metaInfo.put("TimeZoneNames", new TreeSet<String>());
 342         metaInfo.put("CalendarData", new TreeSet<String>());
 343         metaInfo.put("FormatData", new TreeSet<String>());
 344 
 345         for (Bundle bundle : bundles) {
 346             // Get the target map, which contains all the data that should be
 347             // visible for the bundle's locale
 348 
 349             Map<String, Object> targetMap = bundle.getTargetMap();
 350 
 351             EnumSet<Bundle.Type> bundleTypes = bundle.getBundleTypes();
 352 
 353             // Fill in any missing resources in the base bundle from en and en-US data.
 354             // This is because CLDR root.xml is supposed to be language neutral and doesn't
 355             // provide some resource data. Currently, the runtime assumes that there are all
 356             // resources though the parent resource bundle chain.
 357             if (bundle.isRoot()) {
 358                 Map<String, Object> enData = new HashMap<>();
 359                 // Create a superset of en-US and en bundles data in order to
 360                 // fill in any missing resources in the base bundle.
 361                 enData.putAll(Bundle.getBundle("en").getTargetMap());
 362                 enData.putAll(Bundle.getBundle("en_US").getTargetMap());
 363                 for (String key : enData.keySet()) {
 364                     if (!targetMap.containsKey(key)) {
 365                         targetMap.put(key, enData.get(key));
 366                     }
 367                 }
 368                 // Add DateTimePatternChars because CLDR no longer supports localized patterns.
 369                 targetMap.put("DateTimePatternChars", "GyMdkHmsSEDFwWahKzZ");
 370             }
 371 
 372             // Now the map contains just the entries that need to be in the resources bundles.
 373             // Go ahead and generate them.
 374             if (bundleTypes.contains(Bundle.Type.LOCALENAMES)) {
 375                 Map<String, Object> localeNamesMap = extractLocaleNames(targetMap, bundle.getID());
 376                 if (!localeNamesMap.isEmpty() || bundle.isRoot()) {
 377                     metaInfo.get("LocaleNames").add(toLanguageTag(bundle.getID()));
 378                     bundleGenerator.generateBundle("util", "LocaleNames", bundle.getID(), true, localeNamesMap, BundleType.OPEN);
 379                 }
 380             }
 381             if (bundleTypes.contains(Bundle.Type.CURRENCYNAMES)) {
 382                 Map<String, Object> currencyNamesMap = extractCurrencyNames(targetMap, bundle.getID(), bundle.getCurrencies());
 383                 if (!currencyNamesMap.isEmpty() || bundle.isRoot()) {
 384                     metaInfo.get("CurrencyNames").add(toLanguageTag(bundle.getID()));
 385                     bundleGenerator.generateBundle("util", "CurrencyNames", bundle.getID(), true, currencyNamesMap, BundleType.OPEN);
 386                 }
 387             }
 388             if (bundleTypes.contains(Bundle.Type.TIMEZONENAMES)) {
 389                 Map<String, Object> zoneNamesMap = extractZoneNames(targetMap, bundle.getID());
 390                 if (!zoneNamesMap.isEmpty() || bundle.isRoot()) {
 391                     metaInfo.get("TimeZoneNames").add(toLanguageTag(bundle.getID()));
 392                     bundleGenerator.generateBundle("util", "TimeZoneNames", bundle.getID(), true, zoneNamesMap, BundleType.TIMEZONE);
 393                 }
 394             }
 395             if (bundleTypes.contains(Bundle.Type.CALENDARDATA)) {
 396                 Map<String, Object> calendarDataMap = extractCalendarData(targetMap, bundle.getID());
 397                 if (!calendarDataMap.isEmpty() || bundle.isRoot()) {
 398                     metaInfo.get("CalendarData").add(toLanguageTag(bundle.getID()));
 399                     bundleGenerator.generateBundle("util", "CalendarData", bundle.getID(), true, calendarDataMap, BundleType.PLAIN);
 400                 }
 401             }
 402             if (bundleTypes.contains(Bundle.Type.FORMATDATA)) {
 403                 Map<String, Object> formatDataMap = extractFormatData(targetMap, bundle.getID());
 404                 // LocaleData.getAvailableLocales depends on having FormatData bundles around
 405                 if (!formatDataMap.isEmpty() || bundle.isRoot()) {
 406                     metaInfo.get("FormatData").add(toLanguageTag(bundle.getID()));
 407                     bundleGenerator.generateBundle("text", "FormatData", bundle.getID(), true, formatDataMap, BundleType.PLAIN);
 408                 }
 409             }
 410 
 411             // For testing
 412             SortedSet<String> allLocales = new TreeSet<>();
 413             allLocales.addAll(metaInfo.get("CurrencyNames"));
 414             allLocales.addAll(metaInfo.get("LocaleNames"));
 415             allLocales.addAll(metaInfo.get("CalendarData"));
 416             allLocales.addAll(metaInfo.get("FormatData"));
 417             metaInfo.put("All", allLocales);
 418         }
 419 
 420         bundleGenerator.generateMetaInfo(metaInfo);
 421     }
 422 
 423     /*
 424      * Returns the language portion of the given id.
 425      * If id is "root", "" is returned.
 426      */
 427     static String getLanguageCode(String id) {
 428         int index = id.indexOf('_');
 429         String lang = null;
 430         if (index != -1) {
 431             lang = id.substring(0, index);
 432         } else {
 433             lang = "root".equals(id) ? "" : id;
 434         }
 435         return lang;
 436     }
 437 
 438     /**
 439      * Examine if the id includes the country (territory) code. If it does, it returns
 440      * the country code.
 441      * Otherwise, it returns null. eg. when the id is "zh_Hans_SG", it return "SG".
 442      */
 443     private static String getCountryCode(String id) {
 444         //Truncate a variant code with '@' if there is any
 445         //(eg. de_DE@collation=phonebook,currency=DOM)
 446         if (id.indexOf('@') != -1) {
 447             id = id.substring(0, id.indexOf('@'));
 448         }
 449         String[] tokens = id.split("_");
 450         for (int index = 1; index < tokens.length; ++index) {
 451             if (tokens[index].length() == 2
 452                     && Character.isLetter(tokens[index].charAt(0))
 453                     && Character.isLetter(tokens[index].charAt(1))) {
 454                 return tokens[index];
 455             }
 456         }
 457         return null;
 458     }
 459 
 460     private static class KeyComparator implements Comparator<String> {
 461         static KeyComparator INSTANCE = new KeyComparator();
 462 
 463         private KeyComparator() {
 464         }
 465 
 466         @Override
 467         public int compare(String o1, String o2) {
 468             int len1 = o1.length();
 469             int len2 = o2.length();
 470             if (!isDigit(o1.charAt(0)) && !isDigit(o2.charAt(0))) {
 471                 // Shorter string comes first unless either starts with a digit.
 472                 if (len1 < len2) {
 473                     return -1;
 474                 }
 475                 if (len1 > len2) {
 476                     return 1;
 477                 }
 478             }
 479             return o1.compareTo(o2);
 480         }
 481 
 482         private boolean isDigit(char c) {
 483             return c >= '0' && c <= '9';
 484         }
 485     }
 486 
 487     private static Map<String, Object> extractLocaleNames(Map<String, Object> map, String id) {
 488         Map<String, Object> localeNames = new TreeMap<>(KeyComparator.INSTANCE);
 489         for (String key : map.keySet()) {
 490             if (key.startsWith(LOCALE_NAME_PREFIX)) {
 491                 localeNames.put(key.substring(LOCALE_NAME_PREFIX.length()), map.get(key));
 492             }
 493         }
 494         return localeNames;
 495     }
 496 
 497     @SuppressWarnings("AssignmentToForLoopParameter")
 498     private static Map<String, Object> extractCurrencyNames(Map<String, Object> map, String id, String names)
 499             throws Exception {
 500         Map<String, Object> currencyNames = new TreeMap<>(KeyComparator.INSTANCE);
 501         for (String key : map.keySet()) {
 502             if (key.startsWith(CURRENCY_NAME_PREFIX)) {
 503                 currencyNames.put(key.substring(CURRENCY_NAME_PREFIX.length()), map.get(key));
 504             } else if (key.startsWith(CURRENCY_SYMBOL_PREFIX)) {
 505                 currencyNames.put(key.substring(CURRENCY_SYMBOL_PREFIX.length()), map.get(key));
 506             }
 507         }
 508         return currencyNames;
 509     }
 510 
 511     private static Map<String, Object> extractZoneNames(Map<String, Object> map, String id) {
 512         Map<String, Object> names = new HashMap<>();
 513         for (String tzid : handlerMetaZones.keySet()) {
 514             String tzKey = TIMEZONE_ID_PREFIX + tzid;
 515             Object data = map.get(tzKey);
 516             if (data instanceof String[]) {
 517                 names.put(tzid, data);
 518             } else {
 519                 String meta = handlerMetaZones.get(tzid);
 520                 if (meta != null) {
 521                     String metaKey = METAZONE_ID_PREFIX + meta;
 522                     data = map.get(metaKey);
 523                     if (data instanceof String[]) {
 524                         // Keep the metazone prefix here.
 525                         names.put(metaKey, data);
 526                         names.put(tzid, meta);
 527                     }
 528                 }
 529             }
 530         }
 531         return names;
 532     }
 533 
 534     private static Map<String, Object> extractCalendarData(Map<String, Object> map, String id) {
 535         Map<String, Object> calendarData = new LinkedHashMap<>();
 536         copyIfPresent(map, "firstDayOfWeek", calendarData);
 537         copyIfPresent(map, "minimalDaysInFirstWeek", calendarData);
 538         return calendarData;
 539     }
 540 
 541     static final String[] FORMAT_DATA_ELEMENTS = {
 542         "MonthNames",
 543         "standalone.MonthNames",
 544         "MonthAbbreviations",
 545         "standalone.MonthAbbreviations",
 546         "MonthNarrows",
 547         "standalone.MonthNarrows",
 548         "DayNames",
 549         "standalone.DayNames",
 550         "DayAbbreviations",
 551         "standalone.DayAbbreviations",
 552         "DayNarrows",
 553         "standalone.DayNarrows",
 554         "QuarterNames",
 555         "standalone.QuarterNames",
 556         "QuarterAbbreviations",
 557         "standalone.QuarterAbbreviations",
 558         "QuarterNarrows",
 559         "standalone.QuarterNarrows",
 560         "AmPmMarkers",
 561         "narrow.AmPmMarkers",
 562         "long.Eras",
 563         "Eras",
 564         "narrow.Eras",
 565         "field.era",
 566         "field.year",
 567         "field.month",
 568         "field.week",
 569         "field.weekday",
 570         "field.dayperiod",
 571         "field.hour",
 572         "field.minute",
 573         "field.second",
 574         "field.zone",
 575         "TimePatterns",
 576         "DatePatterns",
 577         "DateTimePatterns",
 578         "DateTimePatternChars"
 579     };
 580 
 581     private static Map<String, Object> extractFormatData(Map<String, Object> map, String id) {
 582         Map<String, Object> formatData = new LinkedHashMap<>();
 583         for (CalendarType calendarType : CalendarType.values()) {
 584             String prefix = calendarType.keyElementName();
 585             for (String element : FORMAT_DATA_ELEMENTS) {
 586                 String key = prefix + element;
 587                 copyIfPresent(map, "java.time." + key, formatData);
 588                 copyIfPresent(map, key, formatData);
 589             }
 590         }
 591 
 592         // Copy available calendar names
 593         for (String key : map.keySet()) {
 594             if (key.startsWith(CLDRConverter.CALENDAR_NAME_PREFIX)) {
 595                 String type = key.substring(CLDRConverter.CALENDAR_NAME_PREFIX.length());
 596                 for (CalendarType calendarType : CalendarType.values()) {
 597                     if (type.equals(calendarType.lname())) {
 598                         Object value = map.get(key);
 599                         formatData.put(key, value);
 600                         String ukey = CLDRConverter.CALENDAR_NAME_PREFIX + calendarType.uname();
 601                         if (!key.equals(ukey)) {
 602                             formatData.put(ukey, value);
 603                         }
 604                     }
 605                 }
 606             }
 607         }
 608 
 609         copyIfPresent(map, "DefaultNumberingSystem", formatData);
 610 
 611         @SuppressWarnings("unchecked")
 612         List<String> numberingScripts = (List<String>) map.remove("numberingScripts");
 613         if (numberingScripts != null) {
 614             for (String script : numberingScripts) {
 615                 copyIfPresent(map, script + "." + "NumberElements", formatData);
 616             }
 617         } else {
 618             copyIfPresent(map, "NumberElements", formatData);
 619         }
 620         copyIfPresent(map, "NumberPatterns", formatData);
 621         return formatData;
 622     }
 623 
 624     private static void copyIfPresent(Map<String, Object> src, String key, Map<String, Object> dest) {
 625         Object value = src.get(key);
 626         if (value != null) {
 627             dest.put(key, value);
 628         }
 629     }
 630 
 631     // --- code below here is adapted from java.util.Properties ---
 632     private static final String specialSaveCharsJava = "\"";
 633     private static final String specialSaveCharsProperties = "=: \t\r\n\f#!";
 634 
 635     /*
 636      * Converts unicodes to encoded &#92;uxxxx
 637      * and writes out any of the characters in specialSaveChars
 638      * with a preceding slash
 639      */
 640     static String saveConvert(String theString, boolean useJava) {
 641         if (theString == null) {
 642             return "";
 643         }
 644 
 645         String specialSaveChars;
 646         if (useJava) {
 647             specialSaveChars = specialSaveCharsJava;
 648         } else {
 649             specialSaveChars = specialSaveCharsProperties;
 650         }
 651         boolean escapeSpace = false;
 652 
 653         int len = theString.length();
 654         StringBuilder outBuffer = new StringBuilder(len * 2);
 655         Formatter formatter = new Formatter(outBuffer, Locale.ROOT);
 656 
 657         for (int x = 0; x < len; x++) {
 658             char aChar = theString.charAt(x);
 659             switch (aChar) {
 660             case ' ':
 661                 if (x == 0 || escapeSpace) {
 662                     outBuffer.append('\\');
 663                 }
 664                 outBuffer.append(' ');
 665                 break;
 666             case '\\':
 667                 outBuffer.append('\\');
 668                 outBuffer.append('\\');
 669                 break;
 670             case '\t':
 671                 outBuffer.append('\\');
 672                 outBuffer.append('t');
 673                 break;
 674             case '\n':
 675                 outBuffer.append('\\');
 676                 outBuffer.append('n');
 677                 break;
 678             case '\r':
 679                 outBuffer.append('\\');
 680                 outBuffer.append('r');
 681                 break;
 682             case '\f':
 683                 outBuffer.append('\\');
 684                 outBuffer.append('f');
 685                 break;
 686             default:
 687                 if (aChar < 0x0020 || (!USE_UTF8 && aChar > 0x007e)) {
 688                     formatter.format("\\u%04x", (int)aChar);
 689                 } else {
 690                     if (specialSaveChars.indexOf(aChar) != -1) {
 691                         outBuffer.append('\\');
 692                     }
 693                     outBuffer.append(aChar);
 694                 }
 695             }
 696         }
 697         return outBuffer.toString();
 698     }
 699 
 700     private static String toLanguageTag(String locName) {
 701         if (locName.indexOf('_') == -1) {
 702             return locName;
 703         }
 704         String tag = locName.replaceAll("_", "-");
 705         Locale loc = Locale.forLanguageTag(tag);
 706         return loc.toLanguageTag();
 707     }
 708 
 709     private static String toLocaleName(String tag) {
 710         if (tag.indexOf('-') == -1) {
 711             return tag;
 712         }
 713         return tag.replaceAll("-", "_");
 714     }
 715 }