1 /*
2 * Copyright (c) 2016, 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 package jdk.tools.jlink.internal.plugins;
26
27 import java.io.ByteArrayInputStream;
28 import java.io.IOException;
29 import java.io.UncheckedIOException;
30 import java.util.Arrays;
31 import java.util.Collections;
32 import java.util.HashSet;
33 import java.util.IllformedLocaleException;
34 import java.util.Locale;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38 import java.util.function.Predicate;
39 import java.util.regex.Pattern;
40 import java.util.stream.Collectors;
41 import java.util.stream.IntStream;
42 import java.util.stream.Stream;
43 import jdk.internal.org.objectweb.asm.ClassReader;
44 import jdk.tools.jlink.plugin.TransformerPlugin;
45 import jdk.tools.jlink.plugin.Pool;
46 import jdk.tools.jlink.plugin.PluginException;
47 import jdk.tools.jlink.internal.ResourcePrevisitor;
48 import jdk.tools.jlink.internal.StringTable;
49 import jdk.tools.jlink.internal.Utils;
50
51 /**
52 * Plugin to explicitly specify the locale data included in jdk.localedata
53 * module. This plugin provides a jlink command line option "--include-locales"
54 * with an argument. The argument is a list of BCP 47 language tags separated
55 * by a comma. E.g.,
56 *
57 * "jlink --include-locales en,ja,*-IN"
58 *
59 * This option will include locale data for all available English and Japanese
60 * languages, and ones for the country of India. All other locale data are
61 * filtered out on the image creation.
62 *
63 * Here are a few assumptions:
64 *
65 * 0. All locale data in java.base are unconditionally included.
66 * 1. All the selective locale data are in jdk.localedata module
67 * 2. Their package names are constructed by appending ".ext" to
68 * the corresponding ones in java.base module.
69 * 3. Available locales string in LocaleDataMetaInfo class should
70 * start with at least one white space character, e.g., " ar ar-EG ..."
71 * ^
72 */
73 public final class IncludeLocalesPlugin implements TransformerPlugin, ResourcePrevisitor {
74
75 public static final String NAME = "include-locales";
76 private static final String MODULENAME = "jdk.localedata";
77 private static final Set<String> LOCALEDATA_PACKAGES = Set.of(
78 "sun.text.resources.cldr.ext",
79 "sun.text.resources.ext",
80 "sun.util.resources.cldr.ext",
81 "sun.util.resources.cldr.provider",
82 "sun.util.resources.ext",
83 "sun.util.resources.provider");
84 private static final String METAINFONAME = "LocaleDataMetaInfo";
85 private static final String META_FILES =
86 "*module-info.class," +
87 "*LocaleDataProvider*," +
88 "*" + METAINFONAME + "*,";
89 private static final String INCLUDE_LOCALE_FILES =
90 "*sun/text/resources/ext/[^\\/]+_%%.class," +
91 "*sun/util/resources/ext/[^\\/]+_%%.class," +
92 "*sun/text/resources/cldr/ext/[^\\/]+_%%.class," +
93 "*sun/util/resources/cldr/ext/[^\\/]+_%%.class,";
94 private Predicate<String> predicate;
95 private String userParam;
96 private List<Locale.LanguageRange> priorityList;
97 private List<Locale> available;
98 private List<String> filtered;
99
100 // Special COMPAT provider locales
101 private static final String jaJPJPTag = "ja-JP-JP";
102 private static final String noNONYTag = "no-NO-NY";
103 private static final String thTHTHTag = "th-TH-TH";
104 private static final Locale jaJPJP = new Locale("ja", "JP", "JP");
105 private static final Locale noNONY = new Locale("no", "NO", "NY");
106 private static final Locale thTHTH = new Locale("th", "TH", "TH");
107
108 @Override
109 public String getName() {
110 return NAME;
111 }
112
113 @Override
114 public void visit(Pool in, Pool out) {
115 in.visit((resource) -> {
116 if (resource.getModule().equals(MODULENAME)) {
117 String path = resource.getPath();
118 resource = predicate.test(path) ? resource: null;
119 if (resource != null) {
120 byte[] bytes = resource.getBytes();
121 ClassReader cr = new ClassReader(bytes);
122 if (Arrays.stream(cr.getInterfaces())
123 .anyMatch(i -> i.contains(METAINFONAME)) &&
124 stripUnsupportedLocales(bytes, cr)) {
125 resource = new Pool.ModuleData(MODULENAME, path,
126 resource.getType(),
127 new ByteArrayInputStream(bytes), bytes.length);
128 }
129 }
130 }
131 return resource;
132 }, out);
133 }
134
135 @Override
136 public Set<PluginType> getType() {
137 Set<PluginType> set = new HashSet<>();
138 set.add(CATEGORY.FILTER);
139 return Collections.unmodifiableSet(set);
140 }
141
142 @Override
143 public String getDescription() {
144 return PluginsResourceBundle.getDescription(NAME);
145 }
146
147 @Override
148 public boolean hasArguments() {
149 return true;
150 }
151
152 @Override
153 public String getArgumentsDescription() {
154 return PluginsResourceBundle.getArgument(NAME);
155 }
156
157 @Override
158 public void configure(Map<String, String> config) {
159 userParam = config.get(NAME);
160 priorityList = Arrays.stream(userParam.split(","))
161 .map(s -> {
162 try {
163 return new Locale.LanguageRange(s);
164 } catch (IllegalArgumentException iae) {
165 throw new IllegalArgumentException(String.format(
166 PluginsResourceBundle.getMessage(NAME + ".invalidtag"), s));
167 }
168 })
169 .collect(Collectors.toList());
170 }
171
172 @Override
173 public void previsit(Pool resources, StringTable strings) {
174 final Pattern p = Pattern.compile(".*((Data_)|(Names_))(?<tag>.*)\\.class");
175 Pool.Module module = resources.getModule(MODULENAME);
176
177 // jdk.localedata module validation
178 Set<String> packages = module.getAllPackages();
179 if (!packages.containsAll(LOCALEDATA_PACKAGES)) {
180 throw new PluginException(PluginsResourceBundle.getMessage(NAME + ".missingpackages") +
181 LOCALEDATA_PACKAGES.stream()
182 .filter(pn -> !packages.contains(pn))
183 .collect(Collectors.joining(",\n\t")));
184 }
185
186 available = Stream.concat(module.getContent().stream()
187 .map(md -> p.matcher(md.getPath()))
188 .filter(m -> m.matches())
189 .map(m -> m.group("tag").replaceAll("_", "-")),
190 Stream.concat(Stream.of(jaJPJPTag), Stream.of(thTHTHTag)))
191 .distinct()
192 .sorted()
193 .map(IncludeLocalesPlugin::tagToLocale)
194 .collect(Collectors.toList());
195
196 filtered = filterLocales(available);
197
198 if (filtered.isEmpty()) {
199 throw new PluginException(
200 String.format(PluginsResourceBundle.getMessage(NAME + ".nomatchinglocales"), userParam));
201 }
202
203 try {
204 String value = META_FILES + filtered.stream()
205 .map(s -> includeLocaleFilePatterns(s))
206 .collect(Collectors.joining(","));
207 predicate = new ResourceFilter(Utils.listParser.apply(value), false);
208 } catch (IOException ex) {
209 throw new UncheckedIOException(ex);
210 }
211 }
212
213 private String includeLocaleFilePatterns(String tag) {
214 String pTag = tag.replaceAll("-", "_");
215 String files = "";
216 int lastDelimiter = tag.length();
217 String isoSpecial = pTag.matches("^(he|yi|id).*") ?
218 pTag.replaceFirst("he", "iw")
219 .replaceFirst("yi", "ji")
220 .replaceFirst("id", "in") : "";
221
222 // Add tag patterns including parents
223 while (true) {
224 pTag = pTag.substring(0, lastDelimiter);
225 files += INCLUDE_LOCALE_FILES.replaceAll("%%", pTag);
226
227 if (!isoSpecial.isEmpty()) {
228 isoSpecial = isoSpecial.substring(0, lastDelimiter);
229 files += INCLUDE_LOCALE_FILES.replaceAll("%%", isoSpecial);
230 }
231
232 lastDelimiter = pTag.lastIndexOf('_');
233 if (lastDelimiter == -1) {
234 break;
235 }
236 }
237
238 final String lang = pTag;
239
240 // Add possible special locales of the COMPAT provider
241 files += Set.of(jaJPJPTag, noNONYTag, thTHTHTag).stream()
242 .filter(stag -> lang.equals(stag.substring(0,2)))
243 .map(t -> INCLUDE_LOCALE_FILES.replaceAll("%%", t.replaceAll("-", "_")))
244 .collect(Collectors.joining(","));
245
246 // Add possible UN.M49 files (unconditional for now) for each language
247 files += INCLUDE_LOCALE_FILES.replaceAll("%%", lang + "_[0-9]{3}");
248 if (!isoSpecial.isEmpty()) {
249 files += INCLUDE_LOCALE_FILES.replaceAll("%%", isoSpecial + "_[0-9]{3}");
250 }
251
252 // Add Thai BreakIterator related files
253 if (lang.equals("th")) {
254 files += "*sun/text/resources/thai_dict," +
255 "*sun/text/resources/[^\\/]+_th,";
256 }
257
258 // Add Taiwan resource bundles for Hong Kong
259 if (tag.startsWith("zh-HK")) {
260 files += INCLUDE_LOCALE_FILES.replaceAll("%%", "zh_TW");
261 }
262
263 return files;
264 }
265
266 private boolean stripUnsupportedLocales(byte[] bytes, ClassReader cr) {
267 char[] buf = new char[cr.getMaxStringLength()];
268 boolean[] modified = new boolean[1];
269
270 IntStream.range(1, cr.getItemCount())
271 .map(item -> cr.getItem(item))
272 .forEach(itemIndex -> {
273 if (bytes[itemIndex - 1] == 1 && // UTF-8
274 bytes[itemIndex + 2] == (byte)' ') { // fast check for leading space
275 int length = cr.readUnsignedShort(itemIndex);
276 byte[] b = new byte[length];
277 System.arraycopy(bytes, itemIndex + 2, b, 0, length);
278 if (filterOutUnsupportedTags(b)) {
279 // copy back
280 System.arraycopy(b, 0, bytes, itemIndex + 2, length);
281 modified[0] = true;
282 }
283 }
284 });
285
286 return modified[0];
287 }
288
289 private boolean filterOutUnsupportedTags(byte[] b) {
290 List<Locale> locales;
291
292 try {
293 locales = Arrays.asList(new String(b).split(" ")).stream()
294 .filter(tag -> !tag.isEmpty())
295 .map(IncludeLocalesPlugin::tagToLocale)
296 .collect(Collectors.toList());
297 } catch (IllformedLocaleException ile) {
298 // Seems not an available locales string literal.
299 return false;
300 }
301
302 byte[] filteredBytes = filterLocales(locales).stream()
303 .collect(Collectors.joining(" "))
304 .getBytes();
305 System.arraycopy(filteredBytes, 0, b, 0, filteredBytes.length);
306 Arrays.fill(b, filteredBytes.length, b.length, (byte)' ');
307 return true;
308 }
309
310 private List<String> filterLocales(List<Locale> locales) {
311 List<String> ret =
312 Locale.filter(priorityList, locales, Locale.FilteringMode.EXTENDED_FILTERING).stream()
313 .map(loc ->
314 // Locale.filter() does not preserve the case, which is
315 // significant for "variant" equality. Retrieve the original
316 // locales from the pre-filtered list.
317 locales.stream()
318 .filter(l -> l.toString().equalsIgnoreCase(loc.toString()))
319 .findAny()
320 .orElse(Locale.ROOT)
321 .toLanguageTag())
322 .collect(Collectors.toList());
323
324 // no-NO-NY.toLanguageTag() returns "nn-NO", so specially handle it here
325 if (ret.contains("no-NO")) {
326 ret.add(noNONYTag);
327 }
328
329 return ret;
330 }
331
332 private static final Locale.Builder LOCALE_BUILDER = new Locale.Builder();
333 private static Locale tagToLocale(String tag) {
334 // ISO3166 compatibility
335 tag = tag.replaceFirst("^iw", "he").replaceFirst("^ji", "yi").replaceFirst("^in", "id");
336
337 switch (tag) {
338 case jaJPJPTag:
339 return jaJPJP;
340 case noNONYTag:
341 return noNONY;
342 case thTHTHTag:
343 return thTHTH;
344 default:
345 LOCALE_BUILDER.clear();
346 LOCALE_BUILDER.setLanguageTag(tag);
347 return LOCALE_BUILDER.build();
348 }
349 }
350 }
--- EOF ---