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