aoqi@0: /* aoqi@0: * Copyright (c) 2009, Oracle and/or its affiliates. All rights reserved. aoqi@0: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. aoqi@0: * aoqi@0: * This code is free software; you can redistribute it and/or modify it aoqi@0: * under the terms of the GNU General Public License version 2 only, as aoqi@0: * published by the Free Software Foundation. Oracle designates this aoqi@0: * particular file as subject to the "Classpath" exception as provided aoqi@0: * by Oracle in the LICENSE file that accompanied this code. aoqi@0: * aoqi@0: * This code is distributed in the hope that it will be useful, but WITHOUT aoqi@0: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or aoqi@0: * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License aoqi@0: * version 2 for more details (a copy is included in the LICENSE file that aoqi@0: * accompanied this code). aoqi@0: * aoqi@0: * You should have received a copy of the GNU General Public License version aoqi@0: * 2 along with this work; if not, write to the Free Software Foundation, aoqi@0: * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. aoqi@0: * aoqi@0: * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA aoqi@0: * or visit www.oracle.com if you need additional information or have any aoqi@0: * questions. aoqi@0: */ aoqi@0: aoqi@0: package com.sun.xml.internal.dtdparser; aoqi@0: aoqi@0: import java.io.InputStream; aoqi@0: import java.text.FieldPosition; aoqi@0: import java.text.MessageFormat; aoqi@0: import java.util.Hashtable; aoqi@0: import java.util.Locale; aoqi@0: import java.util.MissingResourceException; aoqi@0: import java.util.ResourceBundle; aoqi@0: aoqi@0: aoqi@0: /** aoqi@0: * This class provides support for multi-language string lookup, as needed aoqi@0: * to localize messages from applications supporting multiple languages aoqi@0: * at the same time. One class of such applications is network services, aoqi@0: * such as HTTP servers, which talk to clients who may not be from the aoqi@0: * same locale as the server. This class supports a form of negotiation aoqi@0: * for the language used in presenting a message from some package, where aoqi@0: * both user (client) preferences and application (server) support are aoqi@0: * accounted for when choosing locales and formatting messages. aoqi@0: *

aoqi@0: *

Each package should have a singleton package-private message catalog aoqi@0: * class. This ensures that the correct class loader will always be used to aoqi@0: * access message resources, and minimizes use of memory:

aoqi@0:  * package some.package;
aoqi@0:  * 

aoqi@0: * // "foo" might be public aoqi@0: * class foo { aoqi@0: * ... aoqi@0: * // package private aoqi@0: * static final Catalog messages = new Catalog (); aoqi@0: * static final class Catalog extends MessageCatalog { aoqi@0: * Catalog () { super (Catalog.class); } aoqi@0: * } aoqi@0: * ... aoqi@0: * } aoqi@0: *

aoqi@0: *

aoqi@0: *

Messages for a known client could be generated using code aoqi@0: * something like this:

aoqi@0:  * String clientLanguages [];
aoqi@0:  * Locale clientLocale;
aoqi@0:  * String clientMessage;
aoqi@0:  * 

aoqi@0: * // client languages will probably be provided by client, aoqi@0: * // e.g. by an HTTP/1.1 "Accept-Language" header. aoqi@0: * clientLanguages = new String [] { "en-ca", "fr-ca", "ja", "zh" }; aoqi@0: * clientLocale = foo.messages.chooseLocale (clientLanguages); aoqi@0: * clientMessage = foo.messages.getMessage (clientLocale, aoqi@0: * "fileCount", aoqi@0: * new Object [] { new Integer (numberOfFiles) } aoqi@0: * ); aoqi@0: *

aoqi@0: *

aoqi@0: *

At this time, this class does not include functionality permitting aoqi@0: * messages to be passed around and localized after-the-fact. The consequence aoqi@0: * of this is that the locale for messages must be passed down through layers aoqi@0: * which have no normal reason to support such passdown, or else the system aoqi@0: * default locale must be used instead of the one the client needs. aoqi@0: *

aoqi@0: *


The following guidelines should be used when constructiong aoqi@0: * multi-language applications:
    aoqi@0: *

    aoqi@0: *

  1. Always use chooseLocale to select the aoqi@0: * locale you pass to your getMessage call. This lets your aoqi@0: * applications use IETF standard locale names, and avoids needless aoqi@0: * use of system defaults. aoqi@0: *

    aoqi@0: *

  2. The localized messages for a given package should always go in aoqi@0: * a separate resources sub-package. There are security aoqi@0: * implications; see below. aoqi@0: *

    aoqi@0: *

  3. Make sure that a language name is included in each bundle name, aoqi@0: * so that the developer's locale will not be inadvertently used. That aoqi@0: * is, don't create defaults like resources/Messages.properties aoqi@0: * or resources/Messages.class, since ResourceBundle will choose aoqi@0: * such defaults rather than giving software a chance to choose a more aoqi@0: * appropriate language for its messages. Your message bundles should aoqi@0: * have names like Messages_en.properties (for the "en", or aoqi@0: * English, language) or Messages_ja.class ("ja" indicates the aoqi@0: * Japanese language). aoqi@0: *

    aoqi@0: *

  4. Only use property files for messages in languages which can aoqi@0: * be limited to the ISO Latin/1 (8859-1) characters supported by the aoqi@0: * property file format. (This is mostly Western European languages.) aoqi@0: * Otherwise, subclass ResourceBundle to provide your messages; it is aoqi@0: * simplest to subclass java.util.ListResourceBundle. aoqi@0: *

    aoqi@0: *

  5. Never use another package's message catalog or resource bundles. aoqi@0: * It should not be possible for a change internal to one package (such aoqi@0: * as eliminating or improving messages) to break another package. aoqi@0: *

    aoqi@0: *

aoqi@0: *

aoqi@0: *

The "resources" sub-package can be treated separately from the aoqi@0: * package with which it is associated. That main package may be sealed aoqi@0: * and possibly signed, preventing other software from adding classes to aoqi@0: * the package which would be able to access methods and data which are aoqi@0: * not designed to be publicly accessible. On the other hand, resources aoqi@0: * such as localized messages are often provided after initial product aoqi@0: * shipment, without a full release cycle for the product. Such files aoqi@0: * (text and class files) need to be added to some package. Since they aoqi@0: * should not be added to the main package, the "resources" subpackage is aoqi@0: * used without risking the security or integrity of that main package aoqi@0: * as distributed in its JAR file. aoqi@0: * aoqi@0: * @author David Brownell aoqi@0: * @version 1.1, 00/08/05 aoqi@0: * @see java.util.Locale aoqi@0: * @see java.util.ListResourceBundle aoqi@0: * @see java.text.MessageFormat aoqi@0: */ aoqi@0: // leave this as "abstract" -- each package needs its own subclass, aoqi@0: // else it's not always going to be using the right class loader. aoqi@0: abstract public class MessageCatalog { aoqi@0: private String bundleName; aoqi@0: aoqi@0: /** aoqi@0: * Create a message catalog for use by classes in the same package aoqi@0: * as the specified class. This uses Messages resource aoqi@0: * bundles in the resources sub-package of class passed as aoqi@0: * a parameter. aoqi@0: * aoqi@0: * @param packageMember Class whose package has localized messages aoqi@0: */ aoqi@0: protected MessageCatalog(Class packageMember) { aoqi@0: this(packageMember, "Messages"); aoqi@0: } aoqi@0: aoqi@0: /** aoqi@0: * Create a message catalog for use by classes in the same package aoqi@0: * as the specified class. This uses the specified resource aoqi@0: * bundle name in the resources sub-package of class passed aoqi@0: * as a parameter; for example, resources.Messages. aoqi@0: * aoqi@0: * @param packageMember Class whose package has localized messages aoqi@0: * @param bundle Name of a group of resource bundles aoqi@0: */ aoqi@0: private MessageCatalog(Class packageMember, String bundle) { aoqi@0: int index; aoqi@0: aoqi@0: bundleName = packageMember.getName(); aoqi@0: index = bundleName.lastIndexOf('.'); aoqi@0: if (index == -1) // "ClassName" aoqi@0: bundleName = ""; aoqi@0: else // "some.package.ClassName" aoqi@0: bundleName = bundleName.substring(0, index) + "."; aoqi@0: bundleName = bundleName + "resources." + bundle; aoqi@0: } aoqi@0: aoqi@0: aoqi@0: /** aoqi@0: * Get a message localized to the specified locale, using the message ID aoqi@0: * and package name if no message is available. The locale is normally aoqi@0: * that of the client of a service, chosen with knowledge that both the aoqi@0: * client and this server support that locale. There are two error aoqi@0: * cases: first, when the specified locale is unsupported or null, the aoqi@0: * default locale is used if possible; second, when no bundle supports aoqi@0: * that locale, the message ID and package name are used. aoqi@0: * aoqi@0: * @param locale The locale of the message to use. If this is null, aoqi@0: * the default locale will be used. aoqi@0: * @param messageId The ID of the message to use. aoqi@0: * @return The message, localized as described above. aoqi@0: */ aoqi@0: public String getMessage(Locale locale, aoqi@0: String messageId) { aoqi@0: ResourceBundle bundle; aoqi@0: aoqi@0: // cope with unsupported locale... aoqi@0: if (locale == null) aoqi@0: locale = Locale.getDefault(); aoqi@0: aoqi@0: try { aoqi@0: bundle = ResourceBundle.getBundle(bundleName, locale); aoqi@0: } catch (MissingResourceException e) { aoqi@0: bundle = ResourceBundle.getBundle(bundleName, Locale.ENGLISH); aoqi@0: } aoqi@0: return bundle.getString(messageId); aoqi@0: } aoqi@0: aoqi@0: aoqi@0: /** aoqi@0: * Format a message localized to the specified locale, using the message aoqi@0: * ID with its package name if none is available. The locale is normally aoqi@0: * the client of a service, chosen with knowledge that both the client aoqi@0: * server support that locale. There are two error cases: first, if the aoqi@0: * specified locale is unsupported or null, the default locale is used if aoqi@0: * possible; second, when no bundle supports that locale, the message ID aoqi@0: * and package name are used. aoqi@0: * aoqi@0: * @param locale The locale of the message to use. If this is null, aoqi@0: * the default locale will be used. aoqi@0: * @param messageId The ID of the message format to use. aoqi@0: * @param parameters Used when formatting the message. Objects in aoqi@0: * this list are turned to strings if they are not Strings, Numbers, aoqi@0: * or Dates (that is, if MessageFormat would treat them as errors). aoqi@0: * @return The message, localized as described above. aoqi@0: * @see java.text.MessageFormat aoqi@0: */ aoqi@0: public String getMessage(Locale locale, aoqi@0: String messageId, aoqi@0: Object parameters []) { aoqi@0: if (parameters == null) aoqi@0: return getMessage(locale, messageId); aoqi@0: aoqi@0: // since most messages won't be tested (sigh), be friendly to aoqi@0: // the inevitable developer errors of passing random data types aoqi@0: // to the message formatting code. aoqi@0: for (int i = 0; i < parameters.length; i++) { aoqi@0: if (!(parameters[i] instanceof String) aoqi@0: && !(parameters[i] instanceof Number) aoqi@0: && !(parameters[i] instanceof java.util.Date)) { aoqi@0: if (parameters[i] == null) aoqi@0: parameters[i] = "(null)"; aoqi@0: else aoqi@0: parameters[i] = parameters[i].toString(); aoqi@0: } aoqi@0: } aoqi@0: aoqi@0: // similarly, cope with unsupported locale... aoqi@0: if (locale == null) aoqi@0: locale = Locale.getDefault(); aoqi@0: aoqi@0: // get the appropriately localized MessageFormat object aoqi@0: ResourceBundle bundle; aoqi@0: MessageFormat format; aoqi@0: aoqi@0: try { aoqi@0: bundle = ResourceBundle.getBundle(bundleName, locale); aoqi@0: } catch (MissingResourceException e) { aoqi@0: bundle = ResourceBundle.getBundle(bundleName, Locale.ENGLISH); aoqi@0: /*String retval; aoqi@0: aoqi@0: retval = packagePrefix (messageId); aoqi@0: for (int i = 0; i < parameters.length; i++) { aoqi@0: retval += ' '; aoqi@0: retval += parameters [i]; aoqi@0: } aoqi@0: return retval;*/ aoqi@0: } aoqi@0: format = new MessageFormat(bundle.getString(messageId)); aoqi@0: format.setLocale(locale); aoqi@0: aoqi@0: // return the formatted message aoqi@0: StringBuffer result = new StringBuffer(); aoqi@0: aoqi@0: result = format.format(parameters, result, new FieldPosition(0)); aoqi@0: return result.toString(); aoqi@0: } aoqi@0: aoqi@0: aoqi@0: /** aoqi@0: * Chooses a client locale to use, using the first language specified in aoqi@0: * the list that is supported by this catalog. If none of the specified aoqi@0: * languages is supported, a null value is returned. Such a list of aoqi@0: * languages might be provided in an HTTP/1.1 "Accept-Language" header aoqi@0: * field, or through some other content negotiation mechanism. aoqi@0: *

aoqi@0: *

The language specifiers recognized are RFC 1766 style ("fr" for aoqi@0: * all French, "fr-ca" for Canadian French), although only the strict aoqi@0: * ISO subset (two letter language and country specifiers) is currently aoqi@0: * supported. Java-style locale strings ("fr_CA") are also supported. aoqi@0: * aoqi@0: * @param languages Array of language specifiers, ordered with the most aoqi@0: * preferable one at the front. For example, "en-ca" then "fr-ca", aoqi@0: * followed by "zh_CN". aoqi@0: * @return The most preferable supported locale, or null. aoqi@0: * @see java.util.Locale aoqi@0: */ aoqi@0: public Locale chooseLocale(String languages []) { aoqi@0: if ((languages = canonicalize(languages)) != null) { aoqi@0: for (int i = 0; i < languages.length; i++) aoqi@0: if (isLocaleSupported(languages[i])) aoqi@0: return getLocale(languages[i]); aoqi@0: } aoqi@0: return null; aoqi@0: } aoqi@0: aoqi@0: aoqi@0: // aoqi@0: // Canonicalizes the RFC 1766 style language strings ("en-in") to aoqi@0: // match standard Java usage ("en_IN"), removing strings that don't aoqi@0: // use two character ISO language and country codes. Avoids all aoqi@0: // memory allocations possible, so that if the strings passed in are aoqi@0: // just lowercase ISO codes (a common case) the input is returned. aoqi@0: // aoqi@0: private String[] canonicalize(String languages []) { aoqi@0: boolean didClone = false; aoqi@0: int trimCount = 0; aoqi@0: aoqi@0: if (languages == null) aoqi@0: return languages; aoqi@0: aoqi@0: for (int i = 0; i < languages.length; i++) { aoqi@0: String lang = languages[i]; aoqi@0: int len = lang.length(); aoqi@0: aoqi@0: // no RFC1766 extensions allowed; "zh" and "zh-tw" (etc) are OK aoqi@0: // as are regular locale names with no variant ("de_CH"). aoqi@0: if (!(len == 2 || len == 5)) { aoqi@0: if (!didClone) { aoqi@0: languages = (String[]) languages.clone(); aoqi@0: didClone = true; aoqi@0: } aoqi@0: languages[i] = null; aoqi@0: trimCount++; aoqi@0: continue; aoqi@0: } aoqi@0: aoqi@0: // language code ... if already lowercase, we change nothing aoqi@0: if (len == 2) { aoqi@0: lang = lang.toLowerCase(); aoqi@0: if (lang != languages[i]) { aoqi@0: if (!didClone) { aoqi@0: languages = (String[]) languages.clone(); aoqi@0: didClone = true; aoqi@0: } aoqi@0: languages[i] = lang; aoqi@0: } aoqi@0: continue; aoqi@0: } aoqi@0: aoqi@0: // language_country ... fixup case, force "_" aoqi@0: char buf [] = new char[5]; aoqi@0: aoqi@0: buf[0] = Character.toLowerCase(lang.charAt(0)); aoqi@0: buf[1] = Character.toLowerCase(lang.charAt(1)); aoqi@0: buf[2] = '_'; aoqi@0: buf[3] = Character.toUpperCase(lang.charAt(3)); aoqi@0: buf[4] = Character.toUpperCase(lang.charAt(4)); aoqi@0: if (!didClone) { aoqi@0: languages = (String[]) languages.clone(); aoqi@0: didClone = true; aoqi@0: } aoqi@0: languages[i] = new String(buf); aoqi@0: } aoqi@0: aoqi@0: // purge any shadows of deleted RFC1766 extended language codes aoqi@0: if (trimCount != 0) { aoqi@0: String temp [] = new String[languages.length - trimCount]; aoqi@0: int i; aoqi@0: aoqi@0: for (i = 0, trimCount = 0; i < temp.length; i++) { aoqi@0: while (languages[i + trimCount] == null) aoqi@0: trimCount++; aoqi@0: temp[i] = languages[i + trimCount]; aoqi@0: } aoqi@0: languages = temp; aoqi@0: } aoqi@0: return languages; aoqi@0: } aoqi@0: aoqi@0: aoqi@0: // aoqi@0: // Returns a locale object supporting the specified locale, using aoqi@0: // a small cache to speed up some common languages and reduce the aoqi@0: // needless allocation of memory. aoqi@0: // aoqi@0: private Locale getLocale(String localeName) { aoqi@0: String language, country; aoqi@0: int index; aoqi@0: aoqi@0: index = localeName.indexOf('_'); aoqi@0: if (index == -1) { aoqi@0: // aoqi@0: // Special case the builtin JDK languages aoqi@0: // aoqi@0: if (localeName.equals("de")) aoqi@0: return Locale.GERMAN; aoqi@0: if (localeName.equals("en")) aoqi@0: return Locale.ENGLISH; aoqi@0: if (localeName.equals("fr")) aoqi@0: return Locale.FRENCH; aoqi@0: if (localeName.equals("it")) aoqi@0: return Locale.ITALIAN; aoqi@0: if (localeName.equals("ja")) aoqi@0: return Locale.JAPANESE; aoqi@0: if (localeName.equals("ko")) aoqi@0: return Locale.KOREAN; aoqi@0: if (localeName.equals("zh")) aoqi@0: return Locale.CHINESE; aoqi@0: aoqi@0: language = localeName; aoqi@0: country = ""; aoqi@0: } else { aoqi@0: if (localeName.equals("zh_CN")) aoqi@0: return Locale.SIMPLIFIED_CHINESE; aoqi@0: if (localeName.equals("zh_TW")) aoqi@0: return Locale.TRADITIONAL_CHINESE; aoqi@0: aoqi@0: // aoqi@0: // JDK also has constants for countries: en_GB, en_US, en_CA, aoqi@0: // fr_FR, fr_CA, de_DE, ja_JP, ko_KR. We don't use those. aoqi@0: // aoqi@0: language = localeName.substring(0, index); aoqi@0: country = localeName.substring(index + 1); aoqi@0: } aoqi@0: aoqi@0: return new Locale(language, country); aoqi@0: } aoqi@0: aoqi@0: aoqi@0: // aoqi@0: // cache for isLanguageSupported(), below ... key is a language aoqi@0: // or locale name, value is a Boolean aoqi@0: // aoqi@0: private Hashtable cache = new Hashtable(5); aoqi@0: aoqi@0: aoqi@0: /** aoqi@0: * Returns true iff the specified locale has explicit language support. aoqi@0: * For example, the traditional Chinese locale "zh_TW" has such support aoqi@0: * if there are message bundles suffixed with either "zh_TW" or "zh". aoqi@0: *

aoqi@0: *

This method is used to bypass part of the search path mechanism aoqi@0: * of the ResourceBundle class, specifically the parts which aoqi@0: * force use of default locales and bundles. Such bypassing is required aoqi@0: * in order to enable use of a client's preferred languages. Following aoqi@0: * the above example, if a client prefers "zh_TW" but can also accept aoqi@0: * "ja", this method would be used to detect that there are no "zh_TW" aoqi@0: * resource bundles and hence that "ja" messages should be used. This aoqi@0: * bypasses the ResourceBundle mechanism which will return messages in aoqi@0: * some other locale (picking some hard-to-anticipate default) instead aoqi@0: * of reporting an error and letting the client choose another locale. aoqi@0: * aoqi@0: * @param localeName A standard Java locale name, using two character aoqi@0: * language codes optionally suffixed by country codes. aoqi@0: * @return True iff the language of that locale is supported. aoqi@0: * @see java.util.Locale aoqi@0: */ aoqi@0: public boolean isLocaleSupported(String localeName) { aoqi@0: // aoqi@0: // Use previous results if possible. We expect that the codebase aoqi@0: // is immutable, so we never worry about changing the cache. aoqi@0: // aoqi@0: Boolean value = (Boolean) cache.get(localeName); aoqi@0: aoqi@0: if (value != null) aoqi@0: return value.booleanValue(); aoqi@0: aoqi@0: // aoqi@0: // Try "language_country_variant", then "language_country", aoqi@0: // then finally "language" ... assuming the longest locale name aoqi@0: // is passed. If not, we'll try fewer options. aoqi@0: // aoqi@0: ClassLoader loader = null; aoqi@0: aoqi@0: for (; ;) { aoqi@0: String name = bundleName + "_" + localeName; aoqi@0: aoqi@0: // look up classes ... aoqi@0: try { aoqi@0: Class.forName(name); aoqi@0: cache.put(localeName, Boolean.TRUE); aoqi@0: return true; aoqi@0: } catch (Exception e) { aoqi@0: } aoqi@0: aoqi@0: // ... then property files (only for ISO Latin/1 messages) aoqi@0: InputStream in; aoqi@0: aoqi@0: if (loader == null) aoqi@0: loader = getClass().getClassLoader(); aoqi@0: aoqi@0: name = name.replace('.', '/'); aoqi@0: name = name + ".properties"; aoqi@0: if (loader == null) aoqi@0: in = ClassLoader.getSystemResourceAsStream(name); aoqi@0: else aoqi@0: in = loader.getResourceAsStream(name); aoqi@0: if (in != null) { aoqi@0: cache.put(localeName, Boolean.TRUE); aoqi@0: return true; aoqi@0: } aoqi@0: aoqi@0: int index = localeName.indexOf('_'); aoqi@0: aoqi@0: if (index > 0) aoqi@0: localeName = localeName.substring(0, index); aoqi@0: else aoqi@0: break; aoqi@0: } aoqi@0: aoqi@0: // aoqi@0: // If we got this far, we failed. Remember for later. aoqi@0: // aoqi@0: cache.put(localeName, Boolean.FALSE); aoqi@0: return false; aoqi@0: } aoqi@0: }