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