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