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