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