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