blob: 8ae29c25a4d897470f0ed32ef714233e1b77ef6c [file] [log] [blame]
/*
* $Id$
*
* The Apache Software License, Version 1.1
*
*
* Copyright (c) 2000 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself,
* if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Crimson" and "Apache Software Foundation" must
* not be used to endorse or promote products derived from this
* software without prior written permission. For written
* permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
* nor may "Apache" appear in their name, without prior written
* permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation and was
* originally based on software copyright (c) 1999, Sun Microsystems, Inc.,
* http://www.sun.com. For more information on the Apache Software
* Foundation, please see <http://www.apache.org/>.
*/
package org.apache.xerces.tree;
import java.io.InputStream;
import java.text.FieldPosition;
import java.text.MessageFormat;
import java.util.Hashtable;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
/**
* This class provides support for multi-language string lookup, as needed
* to localize messages from applications supporting multiple languages
* at the same time. One class of such applications is network services,
* such as HTTP servers, which talk to clients who may not be from the
* same locale as the server. This class supports a form of negotiation
* for the language used in presenting a message from some package, where
* both user (client) preferences and application (server) support are
* accounted for when choosing locales and formatting messages.
*
* <P> Each package should have a singleton package-private message catalog
* class. This ensures that the correct class loader will always be used to
* access message resources, and minimizes use of memory: <PRE>
* package <em>some.package</em>;
*
* // "foo" might be public
* class foo {
* ...
* // package private
* static final Catalog messages = new Catalog ();
* static final class Catalog extends MessageCatalog {
* Catalog () { super (Catalog.class); }
* }
* ...
* }
* </PRE>
*
* <P> Messages for a known client could be generated using code
* something like this: <PRE>
* String clientLanguages [];
* Locale clientLocale;
* String clientMessage;
*
* // client languages will probably be provided by client,
* // e.g. by an HTTP/1.1 "Accept-Language" header.
* clientLanguages = new String [] { "en-ca", "fr-ca", "ja", "zh" };
* clientLocale = foo.messages.chooseLocale (clientLanguages);
* clientMessage = foo.messages.getMessage (clientLocale,
* "fileCount",
* new Object [] { new Integer (numberOfFiles) }
* );
* </PRE>
*
* <P> At this time, this class does not include functionality permitting
* messages to be passed around and localized after-the-fact. The consequence
* of this is that the locale for messages must be passed down through layers
* which have no normal reason to support such passdown, or else the system
* default locale must be used instead of the one the client needs.
*
* <P> <hr> The following guidelines should be used when constructiong
* multi-language applications: <OL>
*
* <LI> Always use <a href=#chooseLocale>chooseLocale</a> to select the
* locale you pass to your <code>getMessage</code> call. This lets your
* applications use IETF standard locale names, and avoids needless
* use of system defaults.
*
* <LI> The localized messages for a given package should always go in
* a separate <em>resources</em> sub-package. There are security
* implications; see below.
*
* <LI> Make sure that a language name is included in each bundle name,
* so that the developer's locale will not be inadvertently used. That
* is, don't create defaults like <em>resources/Messages.properties</em>
* or <em>resources/Messages.class</em>, since ResourceBundle will choose
* such defaults rather than giving software a chance to choose a more
* appropriate language for its messages. Your message bundles should
* have names like <em>Messages_en.properties</em> (for the "en", or
* English, language) or <em>Messages_ja.class</em> ("ja" indicates the
* Japanese language).
*
* <LI> Only use property files for messages in languages which can
* be limited to the ISO Latin/1 (8859-1) characters supported by the
* property file format. (This is mostly Western European languages.)
* Otherwise, subclass ResourceBundle to provide your messages; it is
* simplest to subclass <code>java.util.ListResourceBundle</code>.
*
* <LI> Never use another package's message catalog or resource bundles.
* It should not be possible for a change internal to one package (such
* as eliminating or improving messages) to break another package.
*
* </OL>
*
* <P> The "resources" sub-package can be treated separately from the
* package with which it is associated. That main package may be sealed
* and possibly signed, preventing other software from adding classes to
* the package which would be able to access methods and data which are
* not designed to be publicly accessible. On the other hand, resources
* such as localized messages are often provided after initial product
* shipment, without a full release cycle for the product. Such files
* (text and class files) need to be added to some package. Since they
* should not be added to the main package, the "resources" subpackage is
* used without risking the security or integrity of that main package
* as distributed in its JAR file.
*
* @see java.util.Locale
* @see java.util.ListResourceBundle
* @see java.text.MessageFormat
*
* @version 1.10
* @author David Brownell
*/
// leave this as "abstract" -- each package needs its own subclass,
// else it's not always going to be using the right class loader.
abstract public class MessageCatalog {
private String bundleName;
/**
* Create a message catalog for use by classes in the same package
* as the specified class. This uses <em>Messages</em> resource
* bundles in the <em>resources</em> sub-package of class passed as
* a parameter.
*
* @param packageMember Class whose package has localized messages
*/
protected MessageCatalog (Class packageMember)
{
this (packageMember, "Messages");
}
/**
* Create a message catalog for use by classes in the same package
* as the specified class. This uses the specified resource
* bundle name in the <em>resources</em> sub-package of class passed
* as a parameter; for example, <em>resources.Messages</em>.
*
* @param packageMember Class whose package has localized messages
* @param bundle Name of a group of resource bundles
*/
private MessageCatalog (Class packageMember, String bundle)
{
int index;
bundleName = packageMember.getName ();
index = bundleName.lastIndexOf ('.');
if (index == -1) // "ClassName"
bundleName = "";
else // "some.package.ClassName"
bundleName = bundleName.substring (0, index) + ".";
bundleName = bundleName + "resources." + bundle;
}
/**
* Get a message localized to the specified locale, using the message ID
* and package name if no message is available. The locale is normally
* that of the client of a service, chosen with knowledge that both the
* client and this server support that locale. There are two error
* cases: first, when the specified locale is unsupported or null, the
* default locale is used if possible; second, when no bundle supports
* that locale, the message ID and package name are used.
*
* @param locale The locale of the message to use. If this is null,
* the default locale will be used.
* @param messageId The ID of the message to use.
* @return The message, localized as described above.
*/
public String getMessage (
Locale locale,
String messageId
) {
ResourceBundle bundle;
// cope with unsupported locale...
if (locale == null)
locale = Locale.getDefault ();
try {
bundle = ResourceBundle.getBundle (bundleName, locale);
return bundle.getString (messageId);
} catch (MissingResourceException e) {
return packagePrefix (messageId);
}
}
private String packagePrefix (String messageId)
{
String temp = getClass ().getName ();
int index = temp.lastIndexOf ('.');
if (index == -1) // "ClassName"
temp = "";
else // "some.package.ClassName"
temp = temp.substring (0, index);
return temp + '/' + messageId;
}
/**
* Format a message localized to the specified locale, using the message
* ID with its package name if none is available. The locale is normally
* the client of a service, chosen with knowledge that both the client
* server support that locale. There are two error cases: first, if the
* specified locale is unsupported or null, the default locale is used if
* possible; second, when no bundle supports that locale, the message ID
* and package name are used.
*
* @see java.text.MessageFormat
*
* @param locale The locale of the message to use. If this is null,
* the default locale will be used.
* @param messageId The ID of the message format to use.
* @param parameters Used when formatting the message. Objects in
* this list are turned to strings if they are not Strings, Numbers,
* or Dates (that is, if MessageFormat would treat them as errors).
* @return The message, localized as described above.
*/
public String getMessage (
Locale locale,
String messageId,
Object parameters []
) {
if (parameters == null)
return getMessage (locale, messageId);
// since most messages won't be tested (sigh), be friendly to
// the inevitable developer errors of passing random data types
// to the message formatting code.
for (int i = 0; i < parameters.length; i++) {
if (!(parameters[i] instanceof String)
&& !(parameters[i] instanceof Number)
&& !(parameters[i] instanceof java.util.Date)) {
if (parameters [i] == null)
parameters [i] = "(null)";
else
parameters[i] = parameters[i].toString();
}
}
// similarly, cope with unsupported locale...
if (locale == null)
locale = Locale.getDefault ();
// get the appropriately localized MessageFormat object
ResourceBundle bundle;
MessageFormat format;
try {
bundle = ResourceBundle.getBundle (bundleName, locale);
format = new MessageFormat (bundle.getString (messageId));
} catch (MissingResourceException e) {
String retval;
retval = packagePrefix (messageId);
for (int i = 0; i < parameters.length; i++) {
retval += ' ';
retval += parameters [i];
}
return retval;
}
format.setLocale (locale);
// return the formatted message
StringBuffer result = new StringBuffer ();
result = format.format (parameters, result, new FieldPosition (0));
return result.toString ();
}
/**
* Chooses a client locale to use, using the first language specified in
* the list that is supported by this catalog. If none of the specified
* languages is supported, a null value is returned. Such a list of
* languages might be provided in an HTTP/1.1 "Accept-Language" header
* field, or through some other content negotiation mechanism.
*
* <P> The language specifiers recognized are RFC 1766 style ("fr" for
* all French, "fr-ca" for Canadian French), although only the strict
* ISO subset (two letter language and country specifiers) is currently
* supported. Java-style locale strings ("fr_CA") are also supported.
*
* @see java.util.Locale
*
* @param languages Array of language specifiers, ordered with the most
* preferable one at the front. For example, "en-ca" then "fr-ca",
* followed by "zh_CN".
* @return The most preferable supported locale, or null.
*/
public Locale chooseLocale (String languages [])
{
if ((languages = canonicalize (languages)) != null) {
for (int i = 0; i < languages.length; i++)
if (isLocaleSupported (languages [i]))
return getLocale (languages [i]);
}
return null;
}
//
// Canonicalizes the RFC 1766 style language strings ("en-in") to
// match standard Java usage ("en_IN"), removing strings that don't
// use two character ISO language and country codes. Avoids all
// memory allocations possible, so that if the strings passed in are
// just lowercase ISO codes (a common case) the input is returned.
//
private String [] canonicalize (String languages [])
{
boolean didClone = false;
int trimCount = 0;
if (languages == null)
return languages;
for (int i = 0; i < languages.length; i++) {
String lang = languages [i];
int len = lang.length ();
// no RFC1766 extensions allowed; "zh" and "zh-tw" (etc) are OK
// as are regular locale names with no variant ("de_CH").
if (!(len == 2 || len == 5)) {
if (!didClone) {
languages = (String []) languages.clone ();
didClone = true;
}
languages [i] = null;
trimCount++;
continue;
}
// language code ... if already lowercase, we change nothing
if (len == 2) {
lang = lang.toLowerCase ();
if (lang != languages [i]) {
if (!didClone) {
languages = (String []) languages.clone ();
didClone = true;
}
languages [i] = lang;
}
continue;
}
// language_country ... fixup case, force "_"
char buf [] = new char [5];
buf [0] = Character.toLowerCase (lang.charAt (0));
buf [1] = Character.toLowerCase (lang.charAt (1));
buf [2] = '_';
buf [3] = Character.toUpperCase (lang.charAt (3));
buf [4] = Character.toUpperCase (lang.charAt (4));
if (!didClone) {
languages = (String []) languages.clone ();
didClone = true;
}
languages [i] = new String (buf);
}
// purge any shadows of deleted RFC1766 extended language codes
if (trimCount != 0) {
String temp [] = new String [languages.length - trimCount];
int i;
for (i = 0, trimCount = 0; i < temp.length; i++) {
while (languages [i + trimCount] == null)
trimCount++;
temp [i] = languages [i + trimCount];
}
languages = temp;
}
return languages;
}
//
// Returns a locale object supporting the specified locale, using
// a small cache to speed up some common languages and reduce the
// needless allocation of memory.
//
private Locale getLocale (String localeName)
{
String language, country;
int index;
index = localeName.indexOf ('_');
if (index == -1) {
//
// Special case the builtin JDK languages
//
if (localeName.equals ("de"))
return Locale.GERMAN;
if (localeName.equals ("en"))
return Locale.ENGLISH;
if (localeName.equals ("fr"))
return Locale.FRENCH;
if (localeName.equals ("it"))
return Locale.ITALIAN;
if (localeName.equals ("ja"))
return Locale.JAPANESE;
if (localeName.equals ("ko"))
return Locale.KOREAN;
if (localeName.equals ("zh"))
return Locale.CHINESE;
language = localeName;
country = "";
} else {
if (localeName.equals ("zh_CN"))
return Locale.SIMPLIFIED_CHINESE;
if (localeName.equals ("zh_TW"))
return Locale.TRADITIONAL_CHINESE;
//
// JDK also has constants for countries: en_GB, en_US, en_CA,
// fr_FR, fr_CA, de_DE, ja_JP, ko_KR. We don't use those.
//
language = localeName.substring (0, index);
country = localeName.substring (index + 1);
}
return new Locale (language, country);
}
//
// cache for isLanguageSupported(), below ... key is a language
// or locale name, value is a Boolean
//
private Hashtable cache = new Hashtable (5);
/**
* Returns true iff the specified locale has explicit language support.
* For example, the traditional Chinese locale "zh_TW" has such support
* if there are message bundles suffixed with either "zh_TW" or "zh".
*
* <P> This method is used to bypass part of the search path mechanism
* of the <code>ResourceBundle</code> class, specifically the parts which
* force use of default locales and bundles. Such bypassing is required
* in order to enable use of a client's preferred languages. Following
* the above example, if a client prefers "zh_TW" but can also accept
* "ja", this method would be used to detect that there are no "zh_TW"
* resource bundles and hence that "ja" messages should be used. This
* bypasses the ResourceBundle mechanism which will return messages in
* some other locale (picking some hard-to-anticipate default) instead
* of reporting an error and letting the client choose another locale.
*
* @see java.util.Locale
*
* @param localeName A standard Java locale name, using two character
* language codes optionally suffixed by country codes.
* @return True iff the language of that locale is supported.
*/
public boolean isLocaleSupported (String localeName)
{
//
// Use previous results if possible. We expect that the codebase
// is immutable, so we never worry about changing the cache.
//
Boolean value = (Boolean) cache.get (localeName);
if (value != null)
return value.booleanValue ();
//
// Try "language_country_variant", then "language_country",
// then finally "language" ... assuming the longest locale name
// is passed. If not, we'll try fewer options.
//
ClassLoader loader = null;
for (;;) {
String name = bundleName + "_" + localeName;
// look up classes ...
try {
Class.forName (name);
cache.put (localeName, Boolean.TRUE);
return true;
} catch (Exception e) {}
// ... then property files (only for ISO Latin/1 messages)
InputStream in;
if (loader == null)
loader = getClass ().getClassLoader ();
name = name.replace ('.', '/');
name = name + ".properties";
if (loader == null)
in = ClassLoader.getSystemResourceAsStream (name);
else
in = loader.getResourceAsStream (name);
if (in != null) {
cache.put (localeName, Boolean.TRUE);
return true;
}
int index = localeName.indexOf ('_');
if (index > 0)
localeName = localeName.substring (0, index);
else
break;
}
//
// If we got this far, we failed. Remember for later.
//
cache.put (localeName, Boolean.FALSE);
return false;
}
}