| /* |
| * Copyright (c) 2016, 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 jdk.tools.jlink.internal.plugins; |
| |
| import java.util.AbstractMap; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.IllformedLocaleException; |
| import java.util.Locale; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import static java.util.ResourceBundle.Control; |
| import java.util.Set; |
| import java.util.function.Predicate; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import java.util.stream.IntStream; |
| import java.util.stream.Stream; |
| import jdk.internal.org.objectweb.asm.ClassReader; |
| import jdk.tools.jlink.internal.ResourcePrevisitor; |
| import jdk.tools.jlink.internal.StringTable; |
| import jdk.tools.jlink.plugin.ResourcePoolModule; |
| import jdk.tools.jlink.plugin.PluginException; |
| import jdk.tools.jlink.plugin.ResourcePool; |
| import jdk.tools.jlink.plugin.ResourcePoolBuilder; |
| import jdk.tools.jlink.plugin.ResourcePoolEntry; |
| import jdk.tools.jlink.plugin.Plugin; |
| import sun.util.cldr.CLDRBaseLocaleDataMetaInfo; |
| import sun.util.locale.provider.LocaleProviderAdapter; |
| import sun.util.locale.provider.LocaleProviderAdapter.Type; |
| import sun.util.locale.provider.ResourceBundleBasedAdapter; |
| |
| /** |
| * Plugin to explicitly specify the locale data included in jdk.localedata |
| * module. This plugin provides a jlink command line option "--include-locales" |
| * with an argument. The argument is a list of BCP 47 language tags separated |
| * by a comma. E.g., |
| * |
| * "jlink --include-locales en,ja,*-IN" |
| * |
| * This option will include locale data for all available English and Japanese |
| * languages, and ones for the country of India. All other locale data are |
| * filtered out on the image creation. |
| * |
| * Here are a few assumptions: |
| * |
| * 0. All locale data in java.base are unconditionally included. |
| * 1. All the selective locale data are in jdk.localedata module |
| * 2. Their package names are constructed by appending ".ext" to |
| * the corresponding ones in java.base module. |
| * 3. Available locales string in LocaleDataMetaInfo class should |
| * start with at least one white space character, e.g., " ar ar-EG ..." |
| * ^ |
| */ |
| public final class IncludeLocalesPlugin implements Plugin, ResourcePrevisitor { |
| |
| public static final String NAME = "include-locales"; |
| private static final String MODULENAME = "jdk.localedata"; |
| private static final Set<String> LOCALEDATA_PACKAGES = Set.of( |
| "sun.text.resources.cldr.ext", |
| "sun.text.resources.ext", |
| "sun.util.resources.cldr.ext", |
| "sun.util.resources.cldr.provider", |
| "sun.util.resources.ext", |
| "sun.util.resources.provider"); |
| private static final String METAINFONAME = "LocaleDataMetaInfo"; |
| private static final List<String> META_FILES = List.of( |
| ".+module-info.class", |
| ".+LocaleDataProvider.class", |
| ".+" + METAINFONAME + ".class"); |
| private static final List<String> INCLUDE_LOCALE_FILES = List.of( |
| ".+sun/text/resources/ext/[^_]+_", |
| ".+sun/util/resources/ext/[^_]+_", |
| ".+sun/text/resources/cldr/ext/[^_]+_", |
| ".+sun/util/resources/cldr/ext/[^_]+_"); |
| private Predicate<String> predicate; |
| private String userParam; |
| private List<Locale.LanguageRange> priorityList; |
| private List<Locale> available; |
| private List<String> filtered; |
| |
| private static final ResourceBundleBasedAdapter CLDR_ADAPTER = |
| (ResourceBundleBasedAdapter)LocaleProviderAdapter.forType(Type.CLDR); |
| private static final Map<Locale, String[]> CLDR_PARENT_LOCALES = |
| new CLDRBaseLocaleDataMetaInfo().parentLocales(); |
| |
| // Equivalent map |
| private static final Map<String, List<String>> EQUIV_MAP = |
| Stream.concat( |
| // COMPAT equivalence |
| Map.of( |
| "zh-Hans", List.of("zh-Hans", "zh-CN", "zh-SG"), |
| "zh-Hant", List.of("zh-Hant", "zh-HK", "zh-MO", "zh-TW")) |
| .entrySet() |
| .stream(), |
| |
| // CLDR parent locales |
| CLDR_PARENT_LOCALES.entrySet().stream() |
| .map(entry -> { |
| String parent = entry.getKey().toLanguageTag(); |
| List<String> children = new ArrayList<>(); |
| children.add(parent); |
| |
| Arrays.stream(entry.getValue()) |
| .filter(child -> !child.isEmpty()) |
| .flatMap(child -> |
| Stream.concat( |
| Arrays.stream(CLDR_PARENT_LOCALES.getOrDefault( |
| Locale.forLanguageTag(child), new String[0])) |
| .filter(grandchild -> !grandchild.isEmpty()), |
| List.of(child).stream())) |
| .distinct() |
| .forEach(children::add); |
| return new AbstractMap.SimpleEntry<String, List<String>>(parent, children); |
| }) |
| ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); |
| |
| // Special COMPAT provider locales |
| private static final String jaJPJPTag = "ja-JP-JP"; |
| private static final String noNONYTag = "no-NO-NY"; |
| private static final String thTHTHTag = "th-TH-TH"; |
| private static final Locale jaJPJP = new Locale("ja", "JP", "JP"); |
| private static final Locale noNONY = new Locale("no", "NO", "NY"); |
| private static final Locale thTHTH = new Locale("th", "TH", "TH"); |
| |
| @Override |
| public String getName() { |
| return NAME; |
| } |
| |
| @Override |
| public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { |
| in.transformAndCopy((resource) -> { |
| if (resource.moduleName().equals(MODULENAME)) { |
| String path = resource.path(); |
| resource = predicate.test(path) ? resource: null; |
| if (resource != null && |
| resource.type().equals(ResourcePoolEntry.Type.CLASS_OR_RESOURCE)) { |
| byte[] bytes = resource.contentBytes(); |
| ClassReader cr = new ClassReader(bytes); |
| if (Arrays.stream(cr.getInterfaces()) |
| .anyMatch(i -> i.contains(METAINFONAME)) && |
| stripUnsupportedLocales(bytes, cr)) { |
| resource = resource.copyWithContent(bytes); |
| } |
| } |
| } |
| return resource; |
| }, out); |
| |
| return out.build(); |
| } |
| |
| @Override |
| public Category getType() { |
| return Category.FILTER; |
| } |
| |
| @Override |
| public String getDescription() { |
| return PluginsResourceBundle.getDescription(NAME); |
| } |
| |
| @Override |
| public boolean hasArguments() { |
| return true; |
| } |
| |
| @Override |
| public String getArgumentsDescription() { |
| return PluginsResourceBundle.getArgument(NAME); |
| } |
| |
| @Override |
| public void configure(Map<String, String> config) { |
| userParam = config.get(NAME); |
| |
| try { |
| priorityList = Locale.LanguageRange.parse(userParam, EQUIV_MAP); |
| } catch (IllegalArgumentException iae) { |
| throw new IllegalArgumentException(String.format( |
| PluginsResourceBundle.getMessage(NAME + ".invalidtag"), |
| iae.getMessage().replaceFirst("^range=", ""))); |
| } |
| } |
| |
| @Override |
| public void previsit(ResourcePool resources, StringTable strings) { |
| final Pattern p = Pattern.compile(".*((Data_)|(Names_))(?<tag>.*)\\.class"); |
| Optional<ResourcePoolModule> optMod = resources.moduleView().findModule(MODULENAME); |
| |
| // jdk.localedata module validation |
| if (optMod.isPresent()) { |
| ResourcePoolModule module = optMod.get(); |
| Set<String> packages = module.packages(); |
| if (!packages.containsAll(LOCALEDATA_PACKAGES)) { |
| throw new PluginException(PluginsResourceBundle.getMessage(NAME + ".missingpackages") + |
| LOCALEDATA_PACKAGES.stream() |
| .filter(pn -> !packages.contains(pn)) |
| .collect(Collectors.joining(",\n\t"))); |
| } |
| |
| available = Stream.concat(module.entries() |
| .map(md -> p.matcher(md.path())) |
| .filter(m -> m.matches()) |
| .map(m -> m.group("tag").replaceAll("_", "-")), |
| Stream.concat(Stream.of(jaJPJPTag), Stream.of(thTHTHTag))) |
| .distinct() |
| .sorted() |
| .map(IncludeLocalesPlugin::tagToLocale) |
| .collect(Collectors.toList()); |
| } else { |
| // jdk.localedata is not added. |
| throw new PluginException(PluginsResourceBundle.getMessage(NAME + ".localedatanotfound")); |
| } |
| |
| filtered = filterLocales(available); |
| |
| if (filtered.isEmpty()) { |
| throw new PluginException( |
| String.format(PluginsResourceBundle.getMessage(NAME + ".nomatchinglocales"), userParam)); |
| } |
| |
| List<String> value = Stream.concat( |
| META_FILES.stream(), |
| filtered.stream().flatMap(s -> includeLocaleFilePatterns(s).stream())) |
| .map(s -> "regex:" + s) |
| .collect(Collectors.toList()); |
| |
| predicate = ResourceFilter.includeFilter(value); |
| } |
| |
| private List<String> includeLocaleFilePatterns(String tag) { |
| // Ignore extension variations |
| if (tag.matches(".+-[a-z]-.+")) { |
| return List.of(); |
| } |
| |
| List<String> files = new ArrayList<>(includeLocaleFiles(tag.replaceAll("-", "_"))); |
| |
| // Add Thai BreakIterator related data files |
| if (tag.equals("th")) { |
| files.add(".+sun/text/resources/thai_dict"); |
| files.add(".+sun/text/resources/[^_]+BreakIteratorData_th"); |
| } |
| |
| // Add Taiwan resource bundles for Hong Kong |
| if (tag.equals("zh-HK")) { |
| files.addAll(includeLocaleFiles("zh_TW")); |
| } |
| |
| return files; |
| } |
| |
| private List<String> includeLocaleFiles(String localeStr) { |
| return INCLUDE_LOCALE_FILES.stream() |
| .map(s -> s + localeStr + ".class") |
| .collect(Collectors.toList()); |
| } |
| |
| private boolean stripUnsupportedLocales(byte[] bytes, ClassReader cr) { |
| char[] buf = new char[cr.getMaxStringLength()]; |
| boolean[] modified = new boolean[1]; |
| |
| IntStream.range(1, cr.getItemCount()) |
| .map(item -> cr.getItem(item)) |
| .forEach(itemIndex -> { |
| if (bytes[itemIndex - 1] == 1 && // UTF-8 |
| bytes[itemIndex + 2] == (byte)' ') { // fast check for leading space |
| int length = cr.readUnsignedShort(itemIndex); |
| byte[] b = new byte[length]; |
| System.arraycopy(bytes, itemIndex + 2, b, 0, length); |
| if (filterOutUnsupportedTags(b)) { |
| // copy back |
| System.arraycopy(b, 0, bytes, itemIndex + 2, length); |
| modified[0] = true; |
| } |
| } |
| }); |
| |
| return modified[0]; |
| } |
| |
| private boolean filterOutUnsupportedTags(byte[] b) { |
| List<Locale> locales; |
| |
| try { |
| locales = Arrays.asList(new String(b).split(" ")).stream() |
| .filter(tag -> !tag.isEmpty()) |
| .map(IncludeLocalesPlugin::tagToLocale) |
| .collect(Collectors.toList()); |
| } catch (IllformedLocaleException ile) { |
| // Seems not an available locales string literal. |
| return false; |
| } |
| |
| byte[] filteredBytes = filterLocales(locales).stream() |
| .collect(Collectors.joining(" ")) |
| .getBytes(); |
| |
| if (filteredBytes.length > b.length) { |
| throw new InternalError("Size of filtered locales is bigger than the original one"); |
| } |
| |
| System.arraycopy(filteredBytes, 0, b, 0, filteredBytes.length); |
| Arrays.fill(b, filteredBytes.length, b.length, (byte)' '); |
| return true; |
| } |
| |
| private List<String> filterLocales(List<Locale> locales) { |
| List<String> ret = |
| Locale.filter(priorityList, locales, Locale.FilteringMode.EXTENDED_FILTERING).stream() |
| .flatMap(loc -> Stream.concat(Control.getNoFallbackControl(Control.FORMAT_DEFAULT) |
| .getCandidateLocales("", loc).stream(), |
| CLDR_ADAPTER.getCandidateLocales("", loc).stream())) |
| .map(loc -> |
| // Locale.filter() does not preserve the case, which is |
| // significant for "variant" equality. Retrieve the original |
| // locales from the pre-filtered list. |
| locales.stream() |
| .filter(l -> l.toString().equalsIgnoreCase(loc.toString())) |
| .findAny() |
| .orElse(Locale.ROOT)) |
| .filter(loc -> !loc.equals(Locale.ROOT)) |
| .flatMap(IncludeLocalesPlugin::localeToTags) |
| .distinct() |
| .collect(Collectors.toList()); |
| |
| return ret; |
| } |
| |
| private static final Locale.Builder LOCALE_BUILDER = new Locale.Builder(); |
| private static Locale tagToLocale(String tag) { |
| // ISO3166 compatibility |
| tag = tag.replaceFirst("^iw", "he").replaceFirst("^ji", "yi").replaceFirst("^in", "id"); |
| |
| // Special COMPAT provider locales |
| switch (tag) { |
| case jaJPJPTag: |
| return jaJPJP; |
| case noNONYTag: |
| return noNONY; |
| case thTHTHTag: |
| return thTHTH; |
| default: |
| LOCALE_BUILDER.clear(); |
| LOCALE_BUILDER.setLanguageTag(tag); |
| return LOCALE_BUILDER.build(); |
| } |
| } |
| |
| private static Stream<String> localeToTags(Locale loc) { |
| String tag = loc.toLanguageTag(); |
| Stream<String> ret = null; |
| |
| switch (loc.getLanguage()) { |
| // ISO3166 compatibility |
| case "iw": |
| ret = List.of(tag, tag.replaceFirst("^he", "iw")).stream(); |
| break; |
| case "in": |
| ret = List.of(tag, tag.replaceFirst("^id", "in")).stream(); |
| break; |
| case "ji": |
| ret = List.of(tag, tag.replaceFirst("^yi", "ji")).stream(); |
| break; |
| |
| // Special COMPAT provider locales |
| case "ja": |
| if (loc.getCountry() == "JP") { |
| ret = List.of(tag, jaJPJPTag).stream(); |
| } |
| break; |
| case "no": |
| case "nn": |
| if (loc.getCountry() == "NO") { |
| ret = List.of(tag, noNONYTag).stream(); |
| } |
| break; |
| case "th": |
| if (loc.getCountry() == "TH") { |
| ret = List.of(tag, thTHTHTag).stream(); |
| } |
| break; |
| } |
| |
| return ret == null ? List.of(tag).stream() : ret; |
| } |
| } |