|
1 /* |
|
2 * Copyright (c) 2009, 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 com.sun.xml.internal.dtdparser; |
|
27 |
|
28 import java.io.InputStream; |
|
29 import java.text.FieldPosition; |
|
30 import java.text.MessageFormat; |
|
31 import java.util.Hashtable; |
|
32 import java.util.Locale; |
|
33 import java.util.MissingResourceException; |
|
34 import java.util.ResourceBundle; |
|
35 |
|
36 |
|
37 /** |
|
38 * This class provides support for multi-language string lookup, as needed |
|
39 * to localize messages from applications supporting multiple languages |
|
40 * at the same time. One class of such applications is network services, |
|
41 * such as HTTP servers, which talk to clients who may not be from the |
|
42 * same locale as the server. This class supports a form of negotiation |
|
43 * for the language used in presenting a message from some package, where |
|
44 * both user (client) preferences and application (server) support are |
|
45 * accounted for when choosing locales and formatting messages. |
|
46 * <p/> |
|
47 * <P> Each package should have a singleton package-private message catalog |
|
48 * class. This ensures that the correct class loader will always be used to |
|
49 * access message resources, and minimizes use of memory: <PRE> |
|
50 * package <em>some.package</em>; |
|
51 * <p/> |
|
52 * // "foo" might be public |
|
53 * class foo { |
|
54 * ... |
|
55 * // package private |
|
56 * static final Catalog messages = new Catalog (); |
|
57 * static final class Catalog extends MessageCatalog { |
|
58 * Catalog () { super (Catalog.class); } |
|
59 * } |
|
60 * ... |
|
61 * } |
|
62 * </PRE> |
|
63 * <p/> |
|
64 * <P> Messages for a known client could be generated using code |
|
65 * something like this: <PRE> |
|
66 * String clientLanguages []; |
|
67 * Locale clientLocale; |
|
68 * String clientMessage; |
|
69 * <p/> |
|
70 * // client languages will probably be provided by client, |
|
71 * // e.g. by an HTTP/1.1 "Accept-Language" header. |
|
72 * clientLanguages = new String [] { "en-ca", "fr-ca", "ja", "zh" }; |
|
73 * clientLocale = foo.messages.chooseLocale (clientLanguages); |
|
74 * clientMessage = foo.messages.getMessage (clientLocale, |
|
75 * "fileCount", |
|
76 * new Object [] { new Integer (numberOfFiles) } |
|
77 * ); |
|
78 * </PRE> |
|
79 * <p/> |
|
80 * <P> At this time, this class does not include functionality permitting |
|
81 * messages to be passed around and localized after-the-fact. The consequence |
|
82 * of this is that the locale for messages must be passed down through layers |
|
83 * which have no normal reason to support such passdown, or else the system |
|
84 * default locale must be used instead of the one the client needs. |
|
85 * <p/> |
|
86 * <P> <hr> The following guidelines should be used when constructiong |
|
87 * multi-language applications: <OL> |
|
88 * <p/> |
|
89 * <LI> Always use <a href=#chooseLocale>chooseLocale</a> to select the |
|
90 * locale you pass to your <code>getMessage</code> call. This lets your |
|
91 * applications use IETF standard locale names, and avoids needless |
|
92 * use of system defaults. |
|
93 * <p/> |
|
94 * <LI> The localized messages for a given package should always go in |
|
95 * a separate <em>resources</em> sub-package. There are security |
|
96 * implications; see below. |
|
97 * <p/> |
|
98 * <LI> Make sure that a language name is included in each bundle name, |
|
99 * so that the developer's locale will not be inadvertently used. That |
|
100 * is, don't create defaults like <em>resources/Messages.properties</em> |
|
101 * or <em>resources/Messages.class</em>, since ResourceBundle will choose |
|
102 * such defaults rather than giving software a chance to choose a more |
|
103 * appropriate language for its messages. Your message bundles should |
|
104 * have names like <em>Messages_en.properties</em> (for the "en", or |
|
105 * English, language) or <em>Messages_ja.class</em> ("ja" indicates the |
|
106 * Japanese language). |
|
107 * <p/> |
|
108 * <LI> Only use property files for messages in languages which can |
|
109 * be limited to the ISO Latin/1 (8859-1) characters supported by the |
|
110 * property file format. (This is mostly Western European languages.) |
|
111 * Otherwise, subclass ResourceBundle to provide your messages; it is |
|
112 * simplest to subclass <code>java.util.ListResourceBundle</code>. |
|
113 * <p/> |
|
114 * <LI> Never use another package's message catalog or resource bundles. |
|
115 * It should not be possible for a change internal to one package (such |
|
116 * as eliminating or improving messages) to break another package. |
|
117 * <p/> |
|
118 * </OL> |
|
119 * <p/> |
|
120 * <P> The "resources" sub-package can be treated separately from the |
|
121 * package with which it is associated. That main package may be sealed |
|
122 * and possibly signed, preventing other software from adding classes to |
|
123 * the package which would be able to access methods and data which are |
|
124 * not designed to be publicly accessible. On the other hand, resources |
|
125 * such as localized messages are often provided after initial product |
|
126 * shipment, without a full release cycle for the product. Such files |
|
127 * (text and class files) need to be added to some package. Since they |
|
128 * should not be added to the main package, the "resources" subpackage is |
|
129 * used without risking the security or integrity of that main package |
|
130 * as distributed in its JAR file. |
|
131 * |
|
132 * @author David Brownell |
|
133 * @version 1.1, 00/08/05 |
|
134 * @see java.util.Locale |
|
135 * @see java.util.ListResourceBundle |
|
136 * @see java.text.MessageFormat |
|
137 */ |
|
138 // leave this as "abstract" -- each package needs its own subclass, |
|
139 // else it's not always going to be using the right class loader. |
|
140 abstract public class MessageCatalog { |
|
141 private String bundleName; |
|
142 |
|
143 /** |
|
144 * Create a message catalog for use by classes in the same package |
|
145 * as the specified class. This uses <em>Messages</em> resource |
|
146 * bundles in the <em>resources</em> sub-package of class passed as |
|
147 * a parameter. |
|
148 * |
|
149 * @param packageMember Class whose package has localized messages |
|
150 */ |
|
151 protected MessageCatalog(Class packageMember) { |
|
152 this(packageMember, "Messages"); |
|
153 } |
|
154 |
|
155 /** |
|
156 * Create a message catalog for use by classes in the same package |
|
157 * as the specified class. This uses the specified resource |
|
158 * bundle name in the <em>resources</em> sub-package of class passed |
|
159 * as a parameter; for example, <em>resources.Messages</em>. |
|
160 * |
|
161 * @param packageMember Class whose package has localized messages |
|
162 * @param bundle Name of a group of resource bundles |
|
163 */ |
|
164 private MessageCatalog(Class packageMember, String bundle) { |
|
165 int index; |
|
166 |
|
167 bundleName = packageMember.getName(); |
|
168 index = bundleName.lastIndexOf('.'); |
|
169 if (index == -1) // "ClassName" |
|
170 bundleName = ""; |
|
171 else // "some.package.ClassName" |
|
172 bundleName = bundleName.substring(0, index) + "."; |
|
173 bundleName = bundleName + "resources." + bundle; |
|
174 } |
|
175 |
|
176 |
|
177 /** |
|
178 * Get a message localized to the specified locale, using the message ID |
|
179 * and package name if no message is available. The locale is normally |
|
180 * that of the client of a service, chosen with knowledge that both the |
|
181 * client and this server support that locale. There are two error |
|
182 * cases: first, when the specified locale is unsupported or null, the |
|
183 * default locale is used if possible; second, when no bundle supports |
|
184 * that locale, the message ID and package name are used. |
|
185 * |
|
186 * @param locale The locale of the message to use. If this is null, |
|
187 * the default locale will be used. |
|
188 * @param messageId The ID of the message to use. |
|
189 * @return The message, localized as described above. |
|
190 */ |
|
191 public String getMessage(Locale locale, |
|
192 String messageId) { |
|
193 ResourceBundle bundle; |
|
194 |
|
195 // cope with unsupported locale... |
|
196 if (locale == null) |
|
197 locale = Locale.getDefault(); |
|
198 |
|
199 try { |
|
200 bundle = ResourceBundle.getBundle(bundleName, locale); |
|
201 } catch (MissingResourceException e) { |
|
202 bundle = ResourceBundle.getBundle(bundleName, Locale.ENGLISH); |
|
203 } |
|
204 return bundle.getString(messageId); |
|
205 } |
|
206 |
|
207 |
|
208 /** |
|
209 * Format a message localized to the specified locale, using the message |
|
210 * ID with its package name if none is available. The locale is normally |
|
211 * the client of a service, chosen with knowledge that both the client |
|
212 * server support that locale. There are two error cases: first, if the |
|
213 * specified locale is unsupported or null, the default locale is used if |
|
214 * possible; second, when no bundle supports that locale, the message ID |
|
215 * and package name are used. |
|
216 * |
|
217 * @param locale The locale of the message to use. If this is null, |
|
218 * the default locale will be used. |
|
219 * @param messageId The ID of the message format to use. |
|
220 * @param parameters Used when formatting the message. Objects in |
|
221 * this list are turned to strings if they are not Strings, Numbers, |
|
222 * or Dates (that is, if MessageFormat would treat them as errors). |
|
223 * @return The message, localized as described above. |
|
224 * @see java.text.MessageFormat |
|
225 */ |
|
226 public String getMessage(Locale locale, |
|
227 String messageId, |
|
228 Object parameters []) { |
|
229 if (parameters == null) |
|
230 return getMessage(locale, messageId); |
|
231 |
|
232 // since most messages won't be tested (sigh), be friendly to |
|
233 // the inevitable developer errors of passing random data types |
|
234 // to the message formatting code. |
|
235 for (int i = 0; i < parameters.length; i++) { |
|
236 if (!(parameters[i] instanceof String) |
|
237 && !(parameters[i] instanceof Number) |
|
238 && !(parameters[i] instanceof java.util.Date)) { |
|
239 if (parameters[i] == null) |
|
240 parameters[i] = "(null)"; |
|
241 else |
|
242 parameters[i] = parameters[i].toString(); |
|
243 } |
|
244 } |
|
245 |
|
246 // similarly, cope with unsupported locale... |
|
247 if (locale == null) |
|
248 locale = Locale.getDefault(); |
|
249 |
|
250 // get the appropriately localized MessageFormat object |
|
251 ResourceBundle bundle; |
|
252 MessageFormat format; |
|
253 |
|
254 try { |
|
255 bundle = ResourceBundle.getBundle(bundleName, locale); |
|
256 } catch (MissingResourceException e) { |
|
257 bundle = ResourceBundle.getBundle(bundleName, Locale.ENGLISH); |
|
258 /*String retval; |
|
259 |
|
260 retval = packagePrefix (messageId); |
|
261 for (int i = 0; i < parameters.length; i++) { |
|
262 retval += ' '; |
|
263 retval += parameters [i]; |
|
264 } |
|
265 return retval;*/ |
|
266 } |
|
267 format = new MessageFormat(bundle.getString(messageId)); |
|
268 format.setLocale(locale); |
|
269 |
|
270 // return the formatted message |
|
271 StringBuffer result = new StringBuffer(); |
|
272 |
|
273 result = format.format(parameters, result, new FieldPosition(0)); |
|
274 return result.toString(); |
|
275 } |
|
276 |
|
277 |
|
278 /** |
|
279 * Chooses a client locale to use, using the first language specified in |
|
280 * the list that is supported by this catalog. If none of the specified |
|
281 * languages is supported, a null value is returned. Such a list of |
|
282 * languages might be provided in an HTTP/1.1 "Accept-Language" header |
|
283 * field, or through some other content negotiation mechanism. |
|
284 * <p/> |
|
285 * <P> The language specifiers recognized are RFC 1766 style ("fr" for |
|
286 * all French, "fr-ca" for Canadian French), although only the strict |
|
287 * ISO subset (two letter language and country specifiers) is currently |
|
288 * supported. Java-style locale strings ("fr_CA") are also supported. |
|
289 * |
|
290 * @param languages Array of language specifiers, ordered with the most |
|
291 * preferable one at the front. For example, "en-ca" then "fr-ca", |
|
292 * followed by "zh_CN". |
|
293 * @return The most preferable supported locale, or null. |
|
294 * @see java.util.Locale |
|
295 */ |
|
296 public Locale chooseLocale(String languages []) { |
|
297 if ((languages = canonicalize(languages)) != null) { |
|
298 for (int i = 0; i < languages.length; i++) |
|
299 if (isLocaleSupported(languages[i])) |
|
300 return getLocale(languages[i]); |
|
301 } |
|
302 return null; |
|
303 } |
|
304 |
|
305 |
|
306 // |
|
307 // Canonicalizes the RFC 1766 style language strings ("en-in") to |
|
308 // match standard Java usage ("en_IN"), removing strings that don't |
|
309 // use two character ISO language and country codes. Avoids all |
|
310 // memory allocations possible, so that if the strings passed in are |
|
311 // just lowercase ISO codes (a common case) the input is returned. |
|
312 // |
|
313 private String[] canonicalize(String languages []) { |
|
314 boolean didClone = false; |
|
315 int trimCount = 0; |
|
316 |
|
317 if (languages == null) |
|
318 return languages; |
|
319 |
|
320 for (int i = 0; i < languages.length; i++) { |
|
321 String lang = languages[i]; |
|
322 int len = lang.length(); |
|
323 |
|
324 // no RFC1766 extensions allowed; "zh" and "zh-tw" (etc) are OK |
|
325 // as are regular locale names with no variant ("de_CH"). |
|
326 if (!(len == 2 || len == 5)) { |
|
327 if (!didClone) { |
|
328 languages = (String[]) languages.clone(); |
|
329 didClone = true; |
|
330 } |
|
331 languages[i] = null; |
|
332 trimCount++; |
|
333 continue; |
|
334 } |
|
335 |
|
336 // language code ... if already lowercase, we change nothing |
|
337 if (len == 2) { |
|
338 lang = lang.toLowerCase(); |
|
339 if (lang != languages[i]) { |
|
340 if (!didClone) { |
|
341 languages = (String[]) languages.clone(); |
|
342 didClone = true; |
|
343 } |
|
344 languages[i] = lang; |
|
345 } |
|
346 continue; |
|
347 } |
|
348 |
|
349 // language_country ... fixup case, force "_" |
|
350 char buf [] = new char[5]; |
|
351 |
|
352 buf[0] = Character.toLowerCase(lang.charAt(0)); |
|
353 buf[1] = Character.toLowerCase(lang.charAt(1)); |
|
354 buf[2] = '_'; |
|
355 buf[3] = Character.toUpperCase(lang.charAt(3)); |
|
356 buf[4] = Character.toUpperCase(lang.charAt(4)); |
|
357 if (!didClone) { |
|
358 languages = (String[]) languages.clone(); |
|
359 didClone = true; |
|
360 } |
|
361 languages[i] = new String(buf); |
|
362 } |
|
363 |
|
364 // purge any shadows of deleted RFC1766 extended language codes |
|
365 if (trimCount != 0) { |
|
366 String temp [] = new String[languages.length - trimCount]; |
|
367 int i; |
|
368 |
|
369 for (i = 0, trimCount = 0; i < temp.length; i++) { |
|
370 while (languages[i + trimCount] == null) |
|
371 trimCount++; |
|
372 temp[i] = languages[i + trimCount]; |
|
373 } |
|
374 languages = temp; |
|
375 } |
|
376 return languages; |
|
377 } |
|
378 |
|
379 |
|
380 // |
|
381 // Returns a locale object supporting the specified locale, using |
|
382 // a small cache to speed up some common languages and reduce the |
|
383 // needless allocation of memory. |
|
384 // |
|
385 private Locale getLocale(String localeName) { |
|
386 String language, country; |
|
387 int index; |
|
388 |
|
389 index = localeName.indexOf('_'); |
|
390 if (index == -1) { |
|
391 // |
|
392 // Special case the builtin JDK languages |
|
393 // |
|
394 if (localeName.equals("de")) |
|
395 return Locale.GERMAN; |
|
396 if (localeName.equals("en")) |
|
397 return Locale.ENGLISH; |
|
398 if (localeName.equals("fr")) |
|
399 return Locale.FRENCH; |
|
400 if (localeName.equals("it")) |
|
401 return Locale.ITALIAN; |
|
402 if (localeName.equals("ja")) |
|
403 return Locale.JAPANESE; |
|
404 if (localeName.equals("ko")) |
|
405 return Locale.KOREAN; |
|
406 if (localeName.equals("zh")) |
|
407 return Locale.CHINESE; |
|
408 |
|
409 language = localeName; |
|
410 country = ""; |
|
411 } else { |
|
412 if (localeName.equals("zh_CN")) |
|
413 return Locale.SIMPLIFIED_CHINESE; |
|
414 if (localeName.equals("zh_TW")) |
|
415 return Locale.TRADITIONAL_CHINESE; |
|
416 |
|
417 // |
|
418 // JDK also has constants for countries: en_GB, en_US, en_CA, |
|
419 // fr_FR, fr_CA, de_DE, ja_JP, ko_KR. We don't use those. |
|
420 // |
|
421 language = localeName.substring(0, index); |
|
422 country = localeName.substring(index + 1); |
|
423 } |
|
424 |
|
425 return new Locale(language, country); |
|
426 } |
|
427 |
|
428 |
|
429 // |
|
430 // cache for isLanguageSupported(), below ... key is a language |
|
431 // or locale name, value is a Boolean |
|
432 // |
|
433 private Hashtable cache = new Hashtable(5); |
|
434 |
|
435 |
|
436 /** |
|
437 * Returns true iff the specified locale has explicit language support. |
|
438 * For example, the traditional Chinese locale "zh_TW" has such support |
|
439 * if there are message bundles suffixed with either "zh_TW" or "zh". |
|
440 * <p/> |
|
441 * <P> This method is used to bypass part of the search path mechanism |
|
442 * of the <code>ResourceBundle</code> class, specifically the parts which |
|
443 * force use of default locales and bundles. Such bypassing is required |
|
444 * in order to enable use of a client's preferred languages. Following |
|
445 * the above example, if a client prefers "zh_TW" but can also accept |
|
446 * "ja", this method would be used to detect that there are no "zh_TW" |
|
447 * resource bundles and hence that "ja" messages should be used. This |
|
448 * bypasses the ResourceBundle mechanism which will return messages in |
|
449 * some other locale (picking some hard-to-anticipate default) instead |
|
450 * of reporting an error and letting the client choose another locale. |
|
451 * |
|
452 * @param localeName A standard Java locale name, using two character |
|
453 * language codes optionally suffixed by country codes. |
|
454 * @return True iff the language of that locale is supported. |
|
455 * @see java.util.Locale |
|
456 */ |
|
457 public boolean isLocaleSupported(String localeName) { |
|
458 // |
|
459 // Use previous results if possible. We expect that the codebase |
|
460 // is immutable, so we never worry about changing the cache. |
|
461 // |
|
462 Boolean value = (Boolean) cache.get(localeName); |
|
463 |
|
464 if (value != null) |
|
465 return value.booleanValue(); |
|
466 |
|
467 // |
|
468 // Try "language_country_variant", then "language_country", |
|
469 // then finally "language" ... assuming the longest locale name |
|
470 // is passed. If not, we'll try fewer options. |
|
471 // |
|
472 ClassLoader loader = null; |
|
473 |
|
474 for (; ;) { |
|
475 String name = bundleName + "_" + localeName; |
|
476 |
|
477 // look up classes ... |
|
478 try { |
|
479 Class.forName(name); |
|
480 cache.put(localeName, Boolean.TRUE); |
|
481 return true; |
|
482 } catch (Exception e) { |
|
483 } |
|
484 |
|
485 // ... then property files (only for ISO Latin/1 messages) |
|
486 InputStream in; |
|
487 |
|
488 if (loader == null) |
|
489 loader = getClass().getClassLoader(); |
|
490 |
|
491 name = name.replace('.', '/'); |
|
492 name = name + ".properties"; |
|
493 if (loader == null) |
|
494 in = ClassLoader.getSystemResourceAsStream(name); |
|
495 else |
|
496 in = loader.getResourceAsStream(name); |
|
497 if (in != null) { |
|
498 cache.put(localeName, Boolean.TRUE); |
|
499 return true; |
|
500 } |
|
501 |
|
502 int index = localeName.indexOf('_'); |
|
503 |
|
504 if (index > 0) |
|
505 localeName = localeName.substring(0, index); |
|
506 else |
|
507 break; |
|
508 } |
|
509 |
|
510 // |
|
511 // If we got this far, we failed. Remember for later. |
|
512 // |
|
513 cache.put(localeName, Boolean.FALSE); |
|
514 return false; |
|
515 } |
|
516 } |