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 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
56 private static String CLDR_BASE = "../CLDR/21.0.1/";
57 static String LOCAL_LDML_DTD;
58 static String LOCAL_SPPL_LDML_DTD;
59 private static String SOURCE_FILE_DIR;
60 private static String SPPL_SOURCE_FILE;
61 private static String NUMBERING_SOURCE_FILE;
62 private static String METAZONES_SOURCE_FILE;
63 private static String LIKELYSUBTAGS_SOURCE_FILE;
64 static String DESTINATION_DIR = "build/gensrc";
65
66 static final String LOCALE_NAME_PREFIX = "locale.displayname.";
67 static final String CURRENCY_SYMBOL_PREFIX = "currency.symbol.";
68 static final String CURRENCY_NAME_PREFIX = "currency.displayname.";
69 static final String CALENDAR_NAME_PREFIX = "calendarname.";
70 static final String TIMEZONE_ID_PREFIX = "timezone.id.";
71 static final String ZONE_NAME_PREFIX = "timezone.displayname.";
72 static final String METAZONE_ID_PREFIX = "metazone.id.";
73 static final String PARENT_LOCALE_PREFIX = "parentLocale.";
74
75 private static SupplementDataParseHandler handlerSuppl;
76 private static LikelySubtagsParseHandler handlerLikelySubtags;
77 static NumberingSystemsParseHandler handlerNumbering;
78 static MetaZonesParseHandler handlerMetaZones;
79 private static BundleGenerator bundleGenerator;
80
81 // java.base module related
82 static boolean isBaseModule = false;
83 static final Set<Locale> BASE_LOCALES = new HashSet<>();
84
85 // "parentLocales" map
86 private static final Map<String, SortedSet<String>> parentLocalesMap = new HashMap<>();
87 private static final ResourceBundle.Control defCon =
88 ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_DEFAULT);
89
90 static enum DraftType {
91 UNCONFIRMED,
92 PROVISIONAL,
93 CONTRIBUTED,
94 APPROVED;
95
96 private static final Map<String, DraftType> map = new HashMap<>();
97 static {
98 for (DraftType dt : values()) {
184
185 case "-help":
186 usage();
187 System.exit(0);
188 break;
189
190 default:
191 throw new RuntimeException();
192 }
193 }
194 } catch (RuntimeException e) {
195 severe("unknown or imcomplete arg(s): " + currentArg);
196 usage();
197 System.exit(1);
198 }
199 }
200
201 // Set up path names
202 LOCAL_LDML_DTD = CLDR_BASE + "/dtd/ldml.dtd";
203 LOCAL_SPPL_LDML_DTD = CLDR_BASE + "/dtd/ldmlSupplemental.dtd";
204 SOURCE_FILE_DIR = CLDR_BASE + "/main";
205 SPPL_SOURCE_FILE = CLDR_BASE + "/supplemental/supplementalData.xml";
206 LIKELYSUBTAGS_SOURCE_FILE = CLDR_BASE + "/supplemental/likelySubtags.xml";
207 NUMBERING_SOURCE_FILE = CLDR_BASE + "/supplemental/numberingSystems.xml";
208 METAZONES_SOURCE_FILE = CLDR_BASE + "/supplemental/metaZones.xml";
209
210 if (BASE_LOCALES.isEmpty()) {
211 setupBaseLocales("en-US");
212 }
213
214 bundleGenerator = new ResourceBundleGenerator();
215
216 // Parse data independent of locales
217 parseSupplemental();
218
219 List<Bundle> bundles = readBundleList();
220 convertBundles(bundles);
221 convertBundles(addedBundles);
222 }
223
224 private static void usage() {
225 errout("Usage: java CLDRConverter [options]%n"
226 + "\t-help output this usage message and exit%n"
227 + "\t-verbose output information%n"
228 + "\t-draft [contributed | approved | provisional | unconfirmed]%n"
229 + "\t\t draft level for using data (default: contributed)%n"
230 + "\t-base dir base directory for CLDR input files%n"
231 + "\t-basemodule generates bundles that go into java.base module%n"
232 + "\t-baselocales loc(,loc)* locales that go into the base module%n"
233 + "\t-o dir output directory (default: ./build/gensrc)%n"
234 + "\t-o dir output directory (defaut: ./build/gensrc)%n"
235 + "\t-utf8 use UTF-8 rather than \\uxxxx (for debug)%n");
236 }
237
238 static void info(String fmt, Object... args) {
239 if (verbose) {
240 System.out.printf(fmt, args);
241 }
297 Locale cldrLoc = Locale.forLanguageTag(toLanguageTag(id));
298 StringBuilder sb = getCandLocales(cldrLoc);
299 if (sb.indexOf("root") == -1) {
300 sb.append("root");
301 }
302 Bundle b = new Bundle(id, sb.toString(), null, null);
303 // Insert the bundle for root at the top so that it will get
304 // processed first.
305 if ("root".equals(id)) {
306 retList.add(0, b);
307 } else {
308 retList.add(b);
309 }
310 }
311 }
312 }
313 return retList;
314 }
315
316 private static final Map<String, Map<String, Object>> cldrBundles = new HashMap<>();
317 // this list will contain additional bundles to be generated for Region dependent Data.
318 private static List<Bundle> addedBundles = new ArrayList<>();
319
320 private static Map<String, SortedSet<String>> metaInfo = new HashMap<>();
321
322 static {
323 // For generating information on supported locales.
324 metaInfo.put("LocaleNames", new TreeSet<>());
325 metaInfo.put("CurrencyNames", new TreeSet<>());
326 metaInfo.put("TimeZoneNames", new TreeSet<>());
327 metaInfo.put("CalendarData", new TreeSet<>());
328 metaInfo.put("FormatData", new TreeSet<>());
329 metaInfo.put("AvailableLocales", new TreeSet<>());
330 }
331
332
333 private static Set<String> calendarDataFields = Set.of("firstDayOfWeek", "minimalDaysInFirstWeek");
334
335 static Map<String, Object> getCLDRBundle(String id) throws Exception {
336 Map<String, Object> bundle = cldrBundles.get(id);
337 if (bundle != null) {
338 return bundle;
339 }
340 SAXParserFactory factory = SAXParserFactory.newInstance();
341 factory.setValidating(true);
342 SAXParser parser = factory.newSAXParser();
343 enableFileAccess(parser);
344 LDMLParseHandler handler = new LDMLParseHandler(id);
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 info("Reading file " + file);
353 parser.parse(file, handler);
354
355 bundle = handler.getData();
356 cldrBundles.put(id, bundle);
357 String country = getCountryCode(id);
358 if (country != null) {
359 bundle = handlerSuppl.getData(country);
360 if (bundle != null) {
361 //merge two maps into one map
362 Map<String, Object> temp = cldrBundles.remove(id);
363 bundle.putAll(temp);
364 cldrBundles.put(id, bundle);
365 }
366 }
367 return bundle;
368 }
369
370 // Parsers for data in "supplemental" directory
371 //
372 private static void parseSupplemental() throws Exception {
373 // Parse SupplementalData file and store the information in the HashMap
374 // Calendar information such as firstDay and minDay are stored in
375 // supplementalData.xml as of CLDR1.4. Individual territory is listed
376 // with its ISO 3166 country code while default is listed using UNM49
377 // region and composition numerical code (001 for World.)
378 //
379 // SupplementalData file also provides the "parent" locales which
380 // are othrwise not to be fallen back. Process them here as well.
381 //
382 info("..... Parsing supplementalData.xml .....");
383 SAXParserFactory factorySuppl = SAXParserFactory.newInstance();
384 factorySuppl.setValidating(true);
385 SAXParser parserSuppl = factorySuppl.newSAXParser();
386 enableFileAccess(parserSuppl);
387 handlerSuppl = new SupplementDataParseHandler();
388 File fileSupply = new File(SPPL_SOURCE_FILE);
389 parserSuppl.parse(fileSupply, handlerSuppl);
390 Map<String, Object> parentData = handlerSuppl.getData("root");
391 parentData.keySet().forEach(key -> {
392 parentLocalesMap.put(key, new TreeSet(
393 Arrays.asList(((String)parentData.get(key)).split(" "))));
394 });
395
396 // Parse numberingSystems to get digit zero character information.
397 SAXParserFactory numberingParser = SAXParserFactory.newInstance();
398 numberingParser.setValidating(true);
399 SAXParser parserNumbering = numberingParser.newSAXParser();
400 enableFileAccess(parserNumbering);
401 handlerNumbering = new NumberingSystemsParseHandler();
402 File fileNumbering = new File(NUMBERING_SOURCE_FILE);
403 parserNumbering.parse(fileNumbering, handlerNumbering);
404
405 // Parse metaZones to create mappings between Olson tzids and CLDR meta zone names
406 info("..... Parsing metaZones.xml .....");
407 SAXParserFactory metazonesParser = SAXParserFactory.newInstance();
408 metazonesParser.setValidating(true);
409 SAXParser parserMetaZones = metazonesParser.newSAXParser();
410 enableFileAccess(parserMetaZones);
411 handlerMetaZones = new MetaZonesParseHandler();
412 File fileMetaZones = new File(METAZONES_SOURCE_FILE);
413 parserMetaZones.parse(fileMetaZones, handlerMetaZones);
414
415 // Parse likelySubtags
416 info("..... Parsing likelySubtags.xml .....");
417 SAXParserFactory likelySubtagsParser = SAXParserFactory.newInstance();
418 likelySubtagsParser.setValidating(true);
419 SAXParser parserLikelySubtags = likelySubtagsParser.newSAXParser();
420 enableFileAccess(parserLikelySubtags);
421 handlerLikelySubtags = new LikelySubtagsParseHandler();
422 File fileLikelySubtags = new File(LIKELYSUBTAGS_SOURCE_FILE);
423 parserLikelySubtags.parse(fileLikelySubtags, handlerLikelySubtags);
424 }
425
426 /**
427 * This method will check if a new region dependent Bundle needs to be
428 * generated for this Locale id and targetMap. New Bundle will be generated
429 * when Locale id has non empty script and country code and targetMap
430 * contains region dependent data. This method will also remove region
431 * dependent data from this targetMap after candidate locales check. E.g. It
432 * will call genRegionDependentBundle() in case of az_Latn_AZ locale and
433 * remove region dependent data from this targetMap so that az_Latn_AZ
434 * bundle will not be created. For az_Cyrl_AZ, new Bundle will be generated
435 * but region dependent data will not be removed from targetMap as its candidate
436 * locales are [az_Cyrl_AZ, az_Cyrl, root], which does not include az_AZ for
437 * fallback.
438 *
439 */
440
441 private static void checkRegionDependentBundle(Map<String, Object> targetMap, String id) {
442 if ((CLDRConverter.getScript(id) != "")
443 && (CLDRConverter.getCountryCode(id) != "")) {
444 Map<String, Object> regionDepDataMap = targetMap
445 .keySet()
446 .stream()
447 .filter(calendarDataFields::contains)
448 .collect(Collectors.toMap(k -> k, targetMap::get));
449 if (!regionDepDataMap.isEmpty()) {
450 Locale cldrLoc = new Locale(CLDRConverter.getLanguageCode(id),
451 CLDRConverter.getCountryCode(id));
452 genRegionDependentBundle(regionDepDataMap, cldrLoc);
453 if (checkCandidateLocales(id, cldrLoc)) {
454 // Remove matchedKeys from this targetMap only if checkCandidateLocales() returns true.
455 regionDepDataMap.keySet().forEach(targetMap::remove);
456 }
457 }
458 }
459 }
460 /**
461 * This method will generate a new Bundle for region dependent data,
462 * minimalDaysInFirstWeek and firstDayOfWeek. Newly generated Bundle will be added
463 * to addedBundles list.
464 */
465 private static void genRegionDependentBundle(Map<String, Object> targetMap, Locale cldrLoc) {
466 String localeId = cldrLoc.toString();
467 StringBuilder sb = getCandLocales(cldrLoc);
468 if (sb.indexOf(localeId) == -1) {
469 sb.append(localeId);
470 }
471 Bundle bundle = new Bundle(localeId, sb.toString(), null, null);
472 cldrBundles.put(localeId, targetMap);
473 addedBundles.add(bundle);
474 }
475
476 private static StringBuilder getCandLocales(Locale cldrLoc) {
477 List<Locale> candList = getCandidateLocales(cldrLoc);
478 StringBuilder sb = new StringBuilder();
479 for (Locale loc : candList) {
480 if (!loc.equals(Locale.ROOT)) {
481 sb.append(toLocaleName(loc.toLanguageTag()));
482 sb.append(",");
483 }
484 }
485 return sb;
486 }
487
488 private static List<Locale> getCandidateLocales(Locale cldrLoc) {
489 List<Locale> candList = new ArrayList<>();
490 candList = applyParentLocales("", defCon.getCandidateLocales("", cldrLoc));
491 return candList;
492 }
493
494 /**
495 * This method will return true, if for a given locale, its language and
496 * country specific locale will exist in runtime lookup path. E.g. it will
497 * return true for bs_Latn_BA.
498 */
499 private static boolean checkCandidateLocales(String id, Locale cldrLoc) {
500 return(getCandidateLocales(Locale.forLanguageTag(id.replaceAll("_", "-")))
501 .contains(cldrLoc));
502 }
503
504 private static void convertBundles(List<Bundle> bundles) throws Exception {
505 // parent locales map. The mappings are put in base metaInfo file
506 // for now.
507 if (isBaseModule) {
508 metaInfo.putAll(parentLocalesMap);
509 }
510
511 for (Bundle bundle : bundles) {
512 // Get the target map, which contains all the data that should be
513 // visible for the bundle's locale
514
515 Map<String, Object> targetMap = bundle.getTargetMap();
516
517 // check if new region DependentBundle needs to be generated for this Locale.
518 checkRegionDependentBundle(targetMap, bundle.getID());
519 EnumSet<Bundle.Type> bundleTypes = bundle.getBundleTypes();
520
521 if (bundle.isRoot()) {
522 // Add DateTimePatternChars because CLDR no longer supports localized patterns.
523 targetMap.put("DateTimePatternChars", "GyMdkHmsSEDFwWahKzZ");
524 }
525
526 // Now the map contains just the entries that need to be in the resources bundles.
527 // Go ahead and generate them.
528 if (bundleTypes.contains(Bundle.Type.LOCALENAMES)) {
529 Map<String, Object> localeNamesMap = extractLocaleNames(targetMap, bundle.getID());
530 if (!localeNamesMap.isEmpty() || bundle.isRoot()) {
531 metaInfo.get("LocaleNames").add(toLanguageTag(bundle.getID()));
532 addLikelySubtags(metaInfo, "LocaleNames", bundle.getID());
533 bundleGenerator.generateBundle("util", "LocaleNames", bundle.getJavaID(), true, localeNamesMap, BundleType.OPEN);
534 }
535 }
536 if (bundleTypes.contains(Bundle.Type.CURRENCYNAMES)) {
537 Map<String, Object> currencyNamesMap = extractCurrencyNames(targetMap, bundle.getID(), bundle.getCurrencies());
538 if (!currencyNamesMap.isEmpty() || bundle.isRoot()) {
539 metaInfo.get("CurrencyNames").add(toLanguageTag(bundle.getID()));
540 addLikelySubtags(metaInfo, "CurrencyNames", bundle.getID());
541 bundleGenerator.generateBundle("util", "CurrencyNames", bundle.getJavaID(), true, currencyNamesMap, BundleType.OPEN);
542 }
543 }
544 if (bundleTypes.contains(Bundle.Type.TIMEZONENAMES)) {
545 Map<String, Object> zoneNamesMap = extractZoneNames(targetMap, bundle.getID());
546 if (!zoneNamesMap.isEmpty() || bundle.isRoot()) {
547 metaInfo.get("TimeZoneNames").add(toLanguageTag(bundle.getID()));
548 addLikelySubtags(metaInfo, "TimeZoneNames", bundle.getID());
549 bundleGenerator.generateBundle("util", "TimeZoneNames", bundle.getJavaID(), true, zoneNamesMap, BundleType.TIMEZONE);
550 }
551 }
552 if (bundleTypes.contains(Bundle.Type.CALENDARDATA)) {
553 Map<String, Object> calendarDataMap = extractCalendarData(targetMap, bundle.getID());
554 if (!calendarDataMap.isEmpty() || bundle.isRoot()) {
555 metaInfo.get("CalendarData").add(toLanguageTag(bundle.getID()));
556 addLikelySubtags(metaInfo, "CalendarData", bundle.getID());
557 bundleGenerator.generateBundle("util", "CalendarData", bundle.getJavaID(), true, calendarDataMap, BundleType.PLAIN);
558 }
559 }
560 if (bundleTypes.contains(Bundle.Type.FORMATDATA)) {
561 Map<String, Object> formatDataMap = extractFormatData(targetMap, bundle.getID());
562 if (!formatDataMap.isEmpty() || bundle.isRoot()) {
563 metaInfo.get("FormatData").add(toLanguageTag(bundle.getID()));
564 addLikelySubtags(metaInfo, "FormatData", bundle.getID());
565 bundleGenerator.generateBundle("text", "FormatData", bundle.getJavaID(), true, formatDataMap, BundleType.PLAIN);
566 }
567 }
568
569 // For AvailableLocales
570 metaInfo.get("AvailableLocales").add(toLanguageTag(bundle.getID()));
571 addLikelySubtags(metaInfo, "AvailableLocales", bundle.getID());
572 }
573 addCldrImplicitLocales(metaInfo);
574 bundleGenerator.generateMetaInfo(metaInfo);
575 }
576
577 /**
578 * These are the Locales that are implicitly supported by CLDR.
579 * Adding them explicitly as likelySubtags here, will ensure that
580 * COMPAT locales do not precede them during ResourceBundle search path.
581 */
582 private static void addCldrImplicitLocales(Map<String, SortedSet<String>> metaInfo) {
583 metaInfo.get("LocaleNames").add("zh-Hans-CN");
584 metaInfo.get("LocaleNames").add("zh-Hans-SG");
585 metaInfo.get("LocaleNames").add("zh-Hant-HK");
586 metaInfo.get("LocaleNames").add("zh-Hant-MO");
587 metaInfo.get("LocaleNames").add("zh-Hant-TW");
588 metaInfo.get("CurrencyNames").add("zh-Hans-CN");
589 metaInfo.get("CurrencyNames").add("zh-Hans-SG");
590 metaInfo.get("CurrencyNames").add("zh-Hant-HK");
591 metaInfo.get("CurrencyNames").add("zh-Hant-MO");
592 metaInfo.get("CurrencyNames").add("zh-Hant-TW");
593 metaInfo.get("TimeZoneNames").add("zh-Hans-CN");
594 metaInfo.get("TimeZoneNames").add("zh-Hans-SG");
595 metaInfo.get("TimeZoneNames").add("zh-Hant-HK");
596 metaInfo.get("TimeZoneNames").add("zh-Hant-MO");
597 metaInfo.get("TimeZoneNames").add("zh-Hant-TW");
598 metaInfo.get("TimeZoneNames").add("zh-HK");
599 metaInfo.get("CalendarData").add("zh-Hans-CN");
600 metaInfo.get("CalendarData").add("zh-Hans-SG");
601 metaInfo.get("CalendarData").add("zh-Hant-HK");
602 metaInfo.get("CalendarData").add("zh-Hant-MO");
603 metaInfo.get("CalendarData").add("zh-Hant-TW");
604 metaInfo.get("FormatData").add("zh-Hans-CN");
605 metaInfo.get("FormatData").add("zh-Hans-SG");
606 metaInfo.get("FormatData").add("zh-Hant-HK");
607 metaInfo.get("FormatData").add("zh-Hant-MO");
608 metaInfo.get("FormatData").add("zh-Hant-TW");
609 }
610 static final Map<String, String> aliases = new HashMap<>();
611
612 /**
613 * Translate the aliases into the real entries in the bundle map.
614 */
615 static void handleAliases(Map<String, Object> bundleMap) {
616 Set bundleKeys = bundleMap.keySet();
617 try {
618 for (String key : aliases.keySet()) {
619 String targetKey = aliases.get(key);
620 if (bundleKeys.contains(targetKey)) {
621 bundleMap.putIfAbsent(key, bundleMap.get(targetKey));
622 }
623 }
624 } catch (Exception ex) {
625 Logger.getLogger(CLDRConverter.class.getName()).log(Level.SEVERE, null, ex);
626 }
627 }
628
629 /*
639 * the country code.
640 * Otherwise, it returns null. eg. when the id is "zh_Hans_SG", it return "SG".
641 * It does NOT return UN M.49 code, e.g., '001', as those three digit numbers cannot
642 * be translated into package names.
643 */
644 static String getCountryCode(String id) {
645 String rgn = getRegionCode(id);
646 return rgn.length() == 2 ? rgn: null;
647 }
648
649 /**
650 * Examine if the id includes the region code. If it does, it returns
651 * the region code.
652 * Otherwise, it returns null. eg. when the id is "zh_Hans_SG", it return "SG".
653 * It DOES return UN M.49 code, e.g., '001', as well as ISO 3166 two letter country codes.
654 */
655 static String getRegionCode(String id) {
656 return Locale.forLanguageTag(id.replaceAll("_", "-")).getCountry();
657 }
658
659 /*
660 * Returns the script portion of the given id.
661 * If id is "root", "" is returned.
662 */
663 static String getScript(String id) {
664 return "root".equals(id) ? "" : Locale.forLanguageTag(id.replaceAll("_", "-")).getScript();
665 }
666
667 private static class KeyComparator implements Comparator<String> {
668 static KeyComparator INSTANCE = new KeyComparator();
669
670 private KeyComparator() {
671 }
672
673 @Override
674 public int compare(String o1, String o2) {
675 int len1 = o1.length();
676 int len2 = o2.length();
677 if (!isDigit(o1.charAt(0)) && !isDigit(o2.charAt(0))) {
678 // Shorter string comes first unless either starts with a digit.
679 if (len1 < len2) {
680 return -1;
681 }
682 if (len1 > len2) {
683 return 1;
684 }
685 }
686 return o1.compareTo(o2);
687 }
688
689 private boolean isDigit(char c) {
690 return c >= '0' && c <= '9';
691 }
692 }
693
694 private static Map<String, Object> extractLocaleNames(Map<String, Object> map, String id) {
695 Map<String, Object> localeNames = new TreeMap<>(KeyComparator.INSTANCE);
696 for (String key : map.keySet()) {
697 if (key.startsWith(LOCALE_NAME_PREFIX)) {
698 localeNames.put(key.substring(LOCALE_NAME_PREFIX.length()), map.get(key));
699 }
700 }
701 return localeNames;
702 }
703
704 @SuppressWarnings("AssignmentToForLoopParameter")
705 private static Map<String, Object> extractCurrencyNames(Map<String, Object> map, String id, String names)
706 throws Exception {
707 Map<String, Object> currencyNames = new TreeMap<>(KeyComparator.INSTANCE);
708 for (String key : map.keySet()) {
709 if (key.startsWith(CURRENCY_NAME_PREFIX)) {
710 currencyNames.put(key.substring(CURRENCY_NAME_PREFIX.length()), map.get(key));
711 } else if (key.startsWith(CURRENCY_SYMBOL_PREFIX)) {
712 currencyNames.put(key.substring(CURRENCY_SYMBOL_PREFIX.length()), map.get(key));
713 }
714 }
715 return currencyNames;
716 }
717
718 private static Map<String, Object> extractZoneNames(Map<String, Object> map, String id) {
719 Map<String, Object> names = new HashMap<>();
720
763 if (data instanceof String[]) {
764 names.put(tzid, data);
765 } else {
766 String meta = handlerMetaZones.get(tzid);
767 if (meta != null) {
768 String metaKey = METAZONE_ID_PREFIX + meta;
769 data = map.get(metaKey);
770 if (data instanceof String[]) {
771 // Keep the metazone prefix here.
772 names.put(metaKey, data);
773 names.put(tzid, meta);
774 }
775 }
776 }
777 }
778 return names;
779 }
780
781 private static Map<String, Object> extractCalendarData(Map<String, Object> map, String id) {
782 Map<String, Object> calendarData = new LinkedHashMap<>();
783 copyIfPresent(map, "firstDayOfWeek", calendarData);
784 copyIfPresent(map, "minimalDaysInFirstWeek", calendarData);
785 return calendarData;
786 }
787
788 static final String[] FORMAT_DATA_ELEMENTS = {
789 "MonthNames",
790 "standalone.MonthNames",
791 "MonthAbbreviations",
792 "standalone.MonthAbbreviations",
793 "MonthNarrows",
794 "standalone.MonthNarrows",
795 "DayNames",
796 "standalone.DayNames",
797 "DayAbbreviations",
798 "standalone.DayAbbreviations",
799 "DayNarrows",
800 "standalone.DayNarrows",
801 "QuarterNames",
802 "standalone.QuarterNames",
803 "QuarterAbbreviations",
804 "standalone.QuarterAbbreviations",
827 "DateTimePatterns",
828 "DateTimePatternChars"
829 };
830
831 private static Map<String, Object> extractFormatData(Map<String, Object> map, String id) {
832 Map<String, Object> formatData = new LinkedHashMap<>();
833 for (CalendarType calendarType : CalendarType.values()) {
834 if (calendarType == CalendarType.GENERIC) {
835 continue;
836 }
837 String prefix = calendarType.keyElementName();
838 for (String element : FORMAT_DATA_ELEMENTS) {
839 String key = prefix + element;
840 copyIfPresent(map, "java.time." + key, formatData);
841 copyIfPresent(map, key, formatData);
842 }
843 }
844
845 for (String key : map.keySet()) {
846 // Copy available calendar names
847 if (key.startsWith(CLDRConverter.CALENDAR_NAME_PREFIX)) {
848 String type = key.substring(CLDRConverter.CALENDAR_NAME_PREFIX.length());
849 for (CalendarType calendarType : CalendarType.values()) {
850 if (calendarType == CalendarType.GENERIC) {
851 continue;
852 }
853 if (type.equals(calendarType.lname())) {
854 Object value = map.get(key);
855 formatData.put(key, value);
856 String ukey = CLDRConverter.CALENDAR_NAME_PREFIX + calendarType.uname();
857 if (!key.equals(ukey)) {
858 formatData.put(ukey, value);
859 }
860 }
861 }
862 }
863 }
864
865 copyIfPresent(map, "DefaultNumberingSystem", formatData);
866
867 @SuppressWarnings("unchecked")
868 List<String> numberingScripts = (List<String>) map.remove("numberingScripts");
869 if (numberingScripts != null) {
870 for (String script : numberingScripts) {
871 copyIfPresent(map, script + "." + "NumberElements", formatData);
872 }
873 } else {
874 copyIfPresent(map, "NumberElements", formatData);
875 }
876 copyIfPresent(map, "NumberPatterns", formatData);
877 return formatData;
878 }
879
880 private static void copyIfPresent(Map<String, Object> src, String key, Map<String, Object> dest) {
881 Object value = src.get(key);
882 if (value != null) {
883 dest.put(key, value);
884 }
885 }
886
887 // --- code below here is adapted from java.util.Properties ---
888 private static final String specialSaveCharsJava = "\"";
889 private static final String specialSaveCharsProperties = "=: \t\r\n\f#!";
890
891 /*
892 * Converts unicodes to encoded \uxxxx
893 * and writes out any of the characters in specialSaveChars
894 * with a preceding slash
895 */
896 static String saveConvert(String theString, boolean useJava) {
|
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()) {
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 }
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 /*
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
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",
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) {
|