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