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

ohair@286: *

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

ohair@286:  * package some.package;
ohair@286:  * 

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

ohair@286: *

ohair@286: *

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

ohair@286:  * String clientLanguages [];
ohair@286:  * Locale clientLocale;
ohair@286:  * String clientMessage;
ohair@286:  * 

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

ohair@286: *

ohair@286: *

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

ohair@286: *


The following guidelines should be used when constructiong ohair@286: * multi-language applications:
    ohair@286: *

    ohair@286: *

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

    ohair@286: *

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

    ohair@286: *

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

    ohair@286: *

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

    ohair@286: *

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

    ohair@286: *

ohair@286: *

ohair@286: *

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

ohair@286: *

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

ohair@286: *

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