blob: 3953f38f653e61832bd8d09a1beab550d08723eb [file]
/*
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package build.tools.cldrconverter;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Formatter;
import java.util.HashSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.stream.Collectors;
class ResourceBundleGenerator implements BundleGenerator {
// preferred timezones - keeping compatibility with JDK1.1 3 letter abbreviations
private static final String[] preferredTZIDs = {
"America/Los_Angeles",
"America/Denver",
"America/Phoenix",
"America/Chicago",
"America/New_York",
"America/Indianapolis",
"Pacific/Honolulu",
"America/Anchorage",
"America/Halifax",
"America/Sitka",
"America/St_Johns",
"Europe/Paris",
// Although CLDR does not support abbreviated zones, handle "GMT" as a
// special case here, as it is specified in the javadoc.
"GMT",
"Africa/Casablanca",
"Asia/Jerusalem",
"Asia/Tokyo",
"Europe/Bucharest",
"Asia/Shanghai",
"UTC",
};
// For duplicated values
private static final String META_VALUE_PREFIX = "metaValue_";
@Override
public void generateBundle(String packageName, String baseName, String localeID, boolean useJava,
Map<String, ?> map, BundleType type) throws IOException {
String suffix = useJava ? ".java" : ".properties";
String dirName = CLDRConverter.DESTINATION_DIR + File.separator + "sun" + File.separator
+ packageName + File.separator + "resources" + File.separator + "cldr";
packageName = packageName + ".resources.cldr";
if (CLDRConverter.isBaseModule ^ isBaseLocale(localeID)) {
return;
}
// Assume that non-base resources go into jdk.localedata
if (!CLDRConverter.isBaseModule) {
dirName = dirName + File.separator + "ext";
packageName = packageName + ".ext";
}
File dir = new File(dirName);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dir, baseName + ("root".equals(localeID) ? "" : "_" + localeID) + suffix);
if (!file.exists()) {
file.createNewFile();
}
CLDRConverter.info("\tWriting file " + file);
String encoding;
if (useJava) {
if (CLDRConverter.USE_UTF8) {
encoding = "utf-8";
} else {
encoding = "us-ascii";
}
} else {
encoding = "iso-8859-1";
}
Formatter fmt = null;
if (type == BundleType.TIMEZONE) {
fmt = new Formatter();
Set<String> metaKeys = new HashSet<>();
for (String key : map.keySet()) {
if (key.startsWith(CLDRConverter.METAZONE_ID_PREFIX)) {
String meta = key.substring(CLDRConverter.METAZONE_ID_PREFIX.length());
String[] value;
value = (String[]) map.get(key);
fmt.format(" final String[] %s = new String[] {\n", meta);
for (String s : value) {
fmt.format(" \"%s\",\n", CLDRConverter.saveConvert(s, useJava));
}
fmt.format(" };\n");
metaKeys.add(key);
}
}
for (String key : metaKeys) {
map.remove(key);
}
// Make it preferred ordered
LinkedHashMap<String, Object> newMap = new LinkedHashMap<>();
for (String preferred : preferredTZIDs) {
if (map.containsKey(preferred)) {
newMap.put(preferred, map.remove(preferred));
} else if (("GMT".equals(preferred) || "UTC".equals(preferred)) &&
metaKeys.contains(CLDRConverter.METAZONE_ID_PREFIX+preferred)) {
newMap.put(preferred, preferred);
}
}
newMap.putAll(map);
map = newMap;
} else {
// generic reduction of duplicated values
Map<String, Object> newMap = new LinkedHashMap<>(map);
Map<BundleEntryValue, BundleEntryValue> dedup = new HashMap<>(map.size());
for (Map.Entry<String, ?> entry : map.entrySet()) {
String key = entry.getKey();
Object val = entry.getValue();
BundleEntryValue newEntry = new BundleEntryValue(key, val);
BundleEntryValue oldEntry = dedup.putIfAbsent(newEntry, newEntry);
if (oldEntry != null) {
if (oldEntry.meta()) {
if (fmt == null) {
fmt = new Formatter();
}
String metaVal = oldEntry.metaKey();
if (val instanceof String[] values) {
fmt.format(" final String[] %s = new String[] {\n", metaVal);
for (String s : values) {
fmt.format(" \"%s\",\n", CLDRConverter.saveConvert(s, useJava));
}
fmt.format(" };\n");
} else {
fmt.format(" final String %s = \"%s\";\n", metaVal, CLDRConverter.saveConvert((String)val, useJava));
}
newMap.put(oldEntry.key, oldEntry.metaKey());
}
newMap.put(key, oldEntry.metaKey());
}
}
map = newMap;
}
try (PrintWriter out = new PrintWriter(file, encoding)) {
// Output copyright headers
out.println(getOpenJDKCopyright());
out.println(CopyrightHeaders.getUnicodeCopyright());
if (useJava) {
out.println("package sun." + packageName + ";\n");
out.printf("import %s;\n\n", type.getPathName());
out.printf("public class %s%s extends %s {\n", baseName, "root".equals(localeID) ? "" : "_" + localeID, type.getClassName());
out.println(" @Override\n" +
" protected final Object[][] getContents() {");
if (fmt != null) {
out.print(fmt.toString());
}
out.println(" final Object[][] data = new Object[][] {");
}
for (String key : map.keySet()) {
if (useJava) {
Object value = map.get(key);
if (value == null) {
CLDRConverter.warning("null value for " + key);
} else if (value instanceof String) {
String valStr = (String)value;
if (type == BundleType.TIMEZONE &&
!key.startsWith(CLDRConverter.EXEMPLAR_CITY_PREFIX) ||
valStr.startsWith(META_VALUE_PREFIX)) {
out.printf(" { \"%s\", %s },\n", key, CLDRConverter.saveConvert(valStr, useJava));
} else {
out.printf(" { \"%s\", \"%s\" },\n", key, CLDRConverter.saveConvert(valStr, useJava));
}
} else if (value instanceof String[]) {
String[] values = (String[]) value;
out.println(" { \"" + key + "\",\n new String[] {");
for (String s : values) {
out.println(" \"" + CLDRConverter.saveConvert(s, useJava) + "\",");
}
out.println(" }\n },");
} else {
throw new RuntimeException("unknown value type: " + value.getClass().getName());
}
} else {
out.println(key + "=" + CLDRConverter.saveConvert((String) map.get(key), useJava));
}
}
if (useJava) {
out.println(" };\n return data;\n }\n}");
}
}
}
private static class BundleEntryValue {
private final String key;
private final Object value;
private final int hashCode;
private String metaKey;
BundleEntryValue(String key, Object value) {
this.key = Objects.requireNonNull(key);
this.value = Objects.requireNonNull(value);
if (value instanceof String) {
hashCode = value.hashCode();
} else if (value instanceof String[] arr) {
hashCode = Arrays.hashCode(arr);
} else {
throw new InternalError("Expected a string or a string array");
}
}
/**
* mark the entry as meta
* @return true if the entry was not meta before, false otherwise
*/
public boolean meta() {
if (metaKey == null) {
metaKey = META_VALUE_PREFIX + key.replaceAll("[\\.-]", "_");
return true;
}
return false;
}
public String metaKey() {
return metaKey;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof BundleEntryValue entry) {
if (value instanceof String s) {
return s.equals(entry.value);
} else if (entry.value instanceof String[] otherVal) {
return Arrays.equals((String[]) value, otherVal);
}
}
return false;
}
}
@Override
public void generateMetaInfo(Map<String, SortedSet<String>> metaInfo) throws IOException {
String dirName = CLDRConverter.DESTINATION_DIR + File.separator + "sun" + File.separator + "util" +
File.separator +
(CLDRConverter.isBaseModule ? "cldr" + File.separator + File.separator :
"resources" + File.separator + "cldr" + File.separator + "provider" + File.separator);
File dir = new File(dirName);
if (!dir.exists()) {
dir.mkdirs();
}
String className =
(CLDRConverter.isBaseModule ? "CLDRBaseLocaleDataMetaInfo" : "CLDRLocaleDataMetaInfo");
File file = new File(dir, className + ".java");
if (!file.exists()) {
file.createNewFile();
}
CLDRConverter.info("Generating file " + file);
try (PrintWriter out = new PrintWriter(file, "us-ascii")) {
out.printf(getOpenJDKCopyright());
out.printf("""
package sun.util.%s;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import sun.util.locale.provider.LocaleDataMetaInfo;
import sun.util.locale.provider.LocaleProviderAdapter;
public class %s implements LocaleDataMetaInfo {
""",
CLDRConverter.isBaseModule ? "cldr" : "resources.cldr.provider",
className);
if (CLDRConverter.isBaseModule) {
out.printf("""
private static final Map<Locale, String[]> parentLocalesMap = HashMap.newHashMap(%d);
private static final Map<String, String> languageAliasMap = HashMap.newHashMap(%d);
static final boolean nonlikelyScript = %s; // package access from CLDRLocaleProviderAdapter
static {
""".formatted(
metaInfo.keySet().stream().filter(k -> k.startsWith(CLDRConverter.PARENT_LOCALE_PREFIX)).count(),
CLDRConverter.handlerSupplMeta.getLanguageAliasData().size(),
Boolean.valueOf(CLDRConverter.nonlikelyScript)));
for (String key : metaInfo.keySet()) {
if (key.startsWith(CLDRConverter.PARENT_LOCALE_PREFIX)) {
String parentTag = key.substring(CLDRConverter.PARENT_LOCALE_PREFIX.length());
if ("root".equals(parentTag)) {
out.printf(" parentLocalesMap.put(Locale.ROOT,\n");
} else {
out.printf(" parentLocalesMap.put(Locale.forLanguageTag(\"%s\"),\n",
parentTag);
}
generateStringArray(metaInfo.get(key), out);
}
}
out.println();
// for languageAliasMap
CLDRConverter.handlerSupplMeta.getLanguageAliasData().forEach((key, value) -> {
out.printf(" languageAliasMap.put(\"%s\", \"%s\");\n", key, value);
});
out.printf(" }\n\n");
// end of static initializer block.
// Delayed initialization section
out.printf("""
private static class CLDRMapHolder {
private static final Map<String, String> tzCanonicalIDMap = HashMap.newHashMap(%d);
private static final Map<String, String> likelyScriptMap = HashMap.newHashMap(%d);
static {
""", CLDRConverter.handlerTimeZone.getData().size(),
metaInfo.keySet().stream().filter(k -> k.startsWith(CLDRConverter.LIKELY_SCRIPT_PREFIX)).count());
CLDRConverter.handlerTimeZone.getData().entrySet().stream()
.forEach(e -> {
String[] ids = ((String)e.getValue()).split("\\s");
out.printf(" tzCanonicalIDMap.put(\"%s\", \"%s\");\n", e.getKey(),
ids[0]);
for (int i = 1; i < ids.length; i++) {
out.printf(" tzCanonicalIDMap.put(\"%s\", \"%s\");\n", ids[i],
ids[0]);
}
});
out.println();
// for likelyScript map
for (String key : metaInfo.keySet()) {
if (key.startsWith(CLDRConverter.LIKELY_SCRIPT_PREFIX)) {
// ensure spaces at the begin/end for delimiting purposes
out.printf(" likelyScriptMap.put(\"%s\", \"%s\");\n",
key.substring(CLDRConverter.LIKELY_SCRIPT_PREFIX.length()),
" " + metaInfo.get(key).stream().collect(Collectors.joining(" ")) + " ");
}
}
out.printf(" }\n }\n");
}
out.println();
out.printf("""
@Override
public LocaleProviderAdapter.Type getType() {
return LocaleProviderAdapter.Type.CLDR;
}
@Override
public String availableLanguageTags(String category) {
return " %s";
}
""",
toLocaleList(applyLanguageAliases(metaInfo.get("AvailableLocales")), false));
if(CLDRConverter.isBaseModule) {
out.printf("""
@Override
public Map<String, String> getLanguageAliasMap() {
return languageAliasMap;
}
@Override
public Map<String, String> tzCanonicalIDs() {
return CLDRMapHolder.tzCanonicalIDMap;
}
public Map<Locale, String[]> parentLocales() {
return parentLocalesMap;
}
// package access from CLDRLocaleProviderAdapter
Map<String, String> likelyScriptMap() {
return CLDRMapHolder.likelyScriptMap;
}
""");
}
out.printf("}\n");
}
}
private static void generateStringArray(SortedSet<String> set, PrintWriter out) throws IOException {
String[] children = toLocaleList(set, true).split(" ");
Arrays.sort(children);
out.printf(" new String[] {\n" +
" ");
int count = 0;
for (int i = 0; i < children.length; i++) {
String child = children[i];
out.printf("\"%s\", ", child);
count += child.length() + 4;
if (i != children.length - 1 && count > 64) {
out.printf("\n ");
count = 0;
}
}
out.printf("\n });\n");
}
private static final Locale.Builder LOCALE_BUILDER = new Locale.Builder();
private static boolean isBaseLocale(String localeID) {
localeID = localeID.replaceAll("-", "_");
Locale locale = LOCALE_BUILDER
.clear()
.setLanguage(CLDRConverter.getLanguageCode(localeID))
.setRegion(CLDRConverter.getRegionCode(localeID))
.setScript(CLDRConverter.getScriptCode(localeID))
.build();
return CLDRConverter.BASE_LOCALES.contains(locale);
}
private static String toLocaleList(SortedSet<String> set, boolean all) {
StringBuilder sb = new StringBuilder(set.size() * 6);
for (String id : set) {
if (!"root".equals(id)) {
if (!all && CLDRConverter.isBaseModule ^ isBaseLocale(id)) {
continue;
}
if (sb.length() > 0) {
sb.append(' ');
}
sb.append(id);
}
}
return sb.toString();
}
private static SortedSet<String> applyLanguageAliases(SortedSet<String> tags) {
CLDRConverter.handlerSupplMeta.getLanguageAliasData().forEach((key, value) -> {
if (tags.remove(key)) {
tags.add(value);
}
});
return tags;
}
private static String getOpenJDKCopyright() {
if (CLDRConverter.jdkHeaderTemplate != null) {
return String.format(CLDRConverter.jdkHeaderTemplate, CLDRConverter.copyrightYear);
} else {
return CopyrightHeaders.getOpenJDKCopyright(CLDRConverter.copyrightYear);
}
}
}