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