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