1 /* 2 * Copyright (c) 1998, 2015, 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 * 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:</p><pre> 50 * package <em>some.package</em>; 51 * 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 * 64 * <p> Messages for a known client could be generated using code 65 * something like this:</p><pre> 66 * String clientLanguages []; 67 * Locale clientLocale; 68 * String clientMessage; 69 * 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 * 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.</p> 85 * 86 * <p> The following guidelines should be used when constructiong 87 * multi-language applications:</p> 88 * <ol> 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.</li> 93 * 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.</li> 97 * 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).</li> 107 * 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>.</li> 113 * 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.</li> 117 *</ol> 118 * 119 * <p>The "resources" sub-package can be treated separately from the 120 * package with which it is associated. That main package may be sealed 121 * and possibly signed, preventing other software from adding classes to 122 * the package which would be able to access methods and data which are 123 * not designed to be publicly accessible. On the other hand, resources 124 * such as localized messages are often provided after initial product 125 * shipment, without a full release cycle for the product. Such files 126 * (text and class files) need to be added to some package. Since they 127 * should not be added to the main package, the "resources" subpackage is 128 * used without risking the security or integrity of that main package 129 * as distributed in its JAR file.</p> 130 * 131 * @author David Brownell 132 * @version 1.1, 00/08/05 133 * @see java.util.Locale 134 * @see java.util.ListResourceBundle 135 * @see java.text.MessageFormat 136 */ 137 // leave this as "abstract" -- each package needs its own subclass, 138 // else it's not always going to be using the right class loader. 139 abstract public class MessageCatalog { 140 private String bundleName; 141 142 /** 143 * Create a message catalog for use by classes in the same package 144 * as the specified class. This uses <em>Messages</em> resource 145 * bundles in the <em>resources</em> sub-package of class passed as 146 * a parameter. 147 * 148 * @param packageMember Class whose package has localized messages 149 */ 150 protected MessageCatalog(Class packageMember) { 151 this(packageMember, "Messages"); 152 } 153 154 /** 155 * Create a message catalog for use by classes in the same package 156 * as the specified class. This uses the specified resource 157 * bundle name in the <em>resources</em> sub-package of class passed 158 * as a parameter; for example, <em>resources.Messages</em>. 159 * 160 * @param packageMember Class whose package has localized messages 161 * @param bundle Name of a group of resource bundles 162 */ 163 private MessageCatalog(Class packageMember, String bundle) { 164 int index; 165 166 bundleName = packageMember.getName(); 167 index = bundleName.lastIndexOf('.'); 168 if (index == -1) // "ClassName" 169 bundleName = ""; 170 else // "some.package.ClassName" 171 bundleName = bundleName.substring(0, index) + "."; 172 bundleName = bundleName + "resources." + bundle; 173 } 174 175 176 /** 177 * Get a message localized to the specified locale, using the message ID 178 * and package name if no message is available. The locale is normally 179 * that of the client of a service, chosen with knowledge that both the 180 * client and this server support that locale. There are two error 181 * cases: first, when the specified locale is unsupported or null, the 182 * default locale is used if possible; second, when no bundle supports 183 * that locale, the message ID and package name are used. 184 * 185 * @param locale The locale of the message to use. If this is null, 186 * the default locale will be used. 187 * @param messageId The ID of the message to use. 188 * @return The message, localized as described above. 189 */ 190 public String getMessage(Locale locale, 191 String messageId) { 192 ResourceBundle bundle; 193 194 // cope with unsupported locale... 195 if (locale == null) 196 locale = Locale.getDefault(); 197 198 try { 199 bundle = ResourceBundle.getBundle(bundleName, locale); 200 } catch (MissingResourceException e) { 201 bundle = ResourceBundle.getBundle(bundleName, Locale.ENGLISH); 202 } 203 return bundle.getString(messageId); 204 } 205 206 207 /** 208 * Format a message localized to the specified locale, using the message 209 * ID with its package name if none is available. The locale is normally 210 * the client of a service, chosen with knowledge that both the client 211 * server support that locale. There are two error cases: first, if the 212 * specified locale is unsupported or null, the default locale is used if 213 * possible; second, when no bundle supports that locale, the message ID 214 * and package name are used. 215 * 216 * @param locale The locale of the message to use. If this is null, 217 * the default locale will be used. 218 * @param messageId The ID of the message format to use. 219 * @param parameters Used when formatting the message. Objects in 220 * this list are turned to strings if they are not Strings, Numbers, 221 * or Dates (that is, if MessageFormat would treat them as errors). 222 * @return The message, localized as described above. 223 * @see java.text.MessageFormat 224 */ 225 public String getMessage(Locale locale, 226 String messageId, 227 Object parameters []) { 228 if (parameters == null) 229 return getMessage(locale, messageId); 230 231 // since most messages won't be tested (sigh), be friendly to 232 // the inevitable developer errors of passing random data types 233 // to the message formatting code. 234 for (int i = 0; i < parameters.length; i++) { 235 if (!(parameters[i] instanceof String) 236 && !(parameters[i] instanceof Number) 237 && !(parameters[i] instanceof java.util.Date)) { 238 if (parameters[i] == null) 239 parameters[i] = "(null)"; 240 else 241 parameters[i] = parameters[i].toString(); 242 } 243 } 244 245 // similarly, cope with unsupported locale... 246 if (locale == null) 247 locale = Locale.getDefault(); 248 249 // get the appropriately localized MessageFormat object 250 ResourceBundle bundle; 251 MessageFormat format; 252 253 try { 254 bundle = ResourceBundle.getBundle(bundleName, locale); 255 } catch (MissingResourceException e) { 256 bundle = ResourceBundle.getBundle(bundleName, Locale.ENGLISH); 257 /*String retval; 258 259 retval = packagePrefix (messageId); 260 for (int i = 0; i < parameters.length; i++) { 261 retval += ' '; 262 retval += parameters [i]; 263 } 264 return retval;*/ 265 } 266 format = new MessageFormat(bundle.getString(messageId)); 267 format.setLocale(locale); 268 269 // return the formatted message 270 StringBuffer result = new StringBuffer(); 271 272 result = format.format(parameters, result, new FieldPosition(0)); 273 return result.toString(); 274 } 275 276 277 /** 278 * Chooses a client locale to use, using the first language specified in 279 * the list that is supported by this catalog. If none of the specified 280 * languages is supported, a null value is returned. Such a list of 281 * languages might be provided in an HTTP/1.1 "Accept-Language" header 282 * field, or through some other content negotiation mechanism. 283 * 284 * <p> The language specifiers recognized are RFC 1766 style ("fr" for 285 * all French, "fr-ca" for Canadian French), although only the strict 286 * ISO subset (two letter language and country specifiers) is currently 287 * supported. Java-style locale strings ("fr_CA") are also supported.</p> 288 * 289 * @param languages Array of language specifiers, ordered with the most 290 * preferable one at the front. For example, "en-ca" then "fr-ca", 291 * followed by "zh_CN". 292 * @return The most preferable supported locale, or null. 293 * @see java.util.Locale 294 */ 295 public Locale chooseLocale(String languages []) { 296 if ((languages = canonicalize(languages)) != null) { 297 for (int i = 0; i < languages.length; i++) 298 if (isLocaleSupported(languages[i])) 299 return getLocale(languages[i]); 300 } 301 return null; 302 } 303 304 305 // 306 // Canonicalizes the RFC 1766 style language strings ("en-in") to 307 // match standard Java usage ("en_IN"), removing strings that don't 308 // use two character ISO language and country codes. Avoids all 309 // memory allocations possible, so that if the strings passed in are 310 // just lowercase ISO codes (a common case) the input is returned. 311 // 312 private String[] canonicalize(String languages []) { 313 boolean didClone = false; 314 int trimCount = 0; 315 316 if (languages == null) 317 return languages; 318 319 for (int i = 0; i < languages.length; i++) { 320 String lang = languages[i]; 321 int len = lang.length(); 322 323 // no RFC1766 extensions allowed; "zh" and "zh-tw" (etc) are OK 324 // as are regular locale names with no variant ("de_CH"). 325 if (!(len == 2 || len == 5)) { 326 if (!didClone) { 327 languages = (String[]) languages.clone(); 328 didClone = true; 329 } 330 languages[i] = null; 331 trimCount++; 332 continue; 333 } 334 335 // language code ... if already lowercase, we change nothing 336 if (len == 2) { 337 lang = lang.toLowerCase(Locale.ENGLISH); 338 if (lang != languages[i]) { 339 if (!didClone) { 340 languages = (String[]) languages.clone(); 341 didClone = true; 342 } 343 languages[i] = lang; 344 } 345 continue; 346 } 347 348 // language_country ... fixup case, force "_" 349 char buf [] = new char[5]; 350 351 buf[0] = Character.toLowerCase(lang.charAt(0)); 352 buf[1] = Character.toLowerCase(lang.charAt(1)); 353 buf[2] = '_'; 354 buf[3] = Character.toUpperCase(lang.charAt(3)); 355 buf[4] = Character.toUpperCase(lang.charAt(4)); 356 if (!didClone) { 357 languages = (String[]) languages.clone(); 358 didClone = true; 359 } 360 languages[i] = new String(buf); 361 } 362 363 // purge any shadows of deleted RFC1766 extended language codes 364 if (trimCount != 0) { 365 String temp [] = new String[languages.length - trimCount]; 366 int i; 367 368 for (i = 0, trimCount = 0; i < temp.length; i++) { 369 while (languages[i + trimCount] == null) 370 trimCount++; 371 temp[i] = languages[i + trimCount]; 372 } 373 languages = temp; 374 } 375 return languages; 376 } 377 378 379 // 380 // Returns a locale object supporting the specified locale, using 381 // a small cache to speed up some common languages and reduce the 382 // needless allocation of memory. 383 // 384 private Locale getLocale(String localeName) { 385 String language, country; 386 int index; 387 388 index = localeName.indexOf('_'); 389 if (index == -1) { 390 // 391 // Special case the builtin JDK languages 392 // 393 if (localeName.equals("de")) 394 return Locale.GERMAN; 395 if (localeName.equals("en")) 396 return Locale.ENGLISH; 397 if (localeName.equals("fr")) 398 return Locale.FRENCH; 399 if (localeName.equals("it")) 400 return Locale.ITALIAN; 401 if (localeName.equals("ja")) 402 return Locale.JAPANESE; 403 if (localeName.equals("ko")) 404 return Locale.KOREAN; 405 if (localeName.equals("zh")) 406 return Locale.CHINESE; 407 408 language = localeName; 409 country = ""; 410 } else { 411 if (localeName.equals("zh_CN")) 412 return Locale.SIMPLIFIED_CHINESE; 413 if (localeName.equals("zh_TW")) 414 return Locale.TRADITIONAL_CHINESE; 415 416 // 417 // JDK also has constants for countries: en_GB, en_US, en_CA, 418 // fr_FR, fr_CA, de_DE, ja_JP, ko_KR. We don't use those. 419 // 420 language = localeName.substring(0, index); 421 country = localeName.substring(index + 1); 422 } 423 424 return new Locale(language, country); 425 } 426 427 428 // 429 // cache for isLanguageSupported(), below ... key is a language 430 // or locale name, value is a Boolean 431 // 432 private Hashtable cache = new Hashtable(5); 433 434 435 /** 436 * Returns true iff the specified locale has explicit language support. 437 * For example, the traditional Chinese locale "zh_TW" has such support 438 * if there are message bundles suffixed with either "zh_TW" or "zh". 439 * 440 * <P> This method is used to bypass part of the search path mechanism 441 * of the <code>ResourceBundle</code> class, specifically the parts which 442 * force use of default locales and bundles. Such bypassing is required 443 * in order to enable use of a client's preferred languages. Following 444 * the above example, if a client prefers "zh_TW" but can also accept 445 * "ja", this method would be used to detect that there are no "zh_TW" 446 * resource bundles and hence that "ja" messages should be used. This 447 * bypasses the ResourceBundle mechanism which will return messages in 448 * some other locale (picking some hard-to-anticipate default) instead 449 * of reporting an error and letting the client choose another locale. 450 * 451 * @param localeName A standard Java locale name, using two character 452 * language codes optionally suffixed by country codes. 453 * @return True iff the language of that locale is supported. 454 * @see java.util.Locale 455 */ 456 public boolean isLocaleSupported(String localeName) { 457 // 458 // Use previous results if possible. We expect that the codebase 459 // is immutable, so we never worry about changing the cache. 460 // 461 Boolean value = (Boolean) cache.get(localeName); 462 463 if (value != null) 464 return value.booleanValue(); 465 466 // 467 // Try "language_country_variant", then "language_country", 468 // then finally "language" ... assuming the longest locale name 469 // is passed. If not, we'll try fewer options. 470 // 471 ClassLoader loader = null; 472 473 for (; ;) { 474 String name = bundleName + "_" + localeName; 475 476 // look up classes ... 477 try { 478 Class.forName(name); 479 cache.put(localeName, Boolean.TRUE); 480 return true; 481 } catch (Exception e) { 482 } 483 484 // ... then property files (only for ISO Latin/1 messages) 485 InputStream in; 486 487 if (loader == null) 488 loader = getClass().getClassLoader(); 489 490 name = name.replace('.', '/'); 491 name = name + ".properties"; 492 if (loader == null) 493 in = ClassLoader.getSystemResourceAsStream(name); 494 else 495 in = loader.getResourceAsStream(name); 496 if (in != null) { 497 cache.put(localeName, Boolean.TRUE); 498 return true; 499 } 500 501 int index = localeName.indexOf('_'); 502 503 if (index > 0) 504 localeName = localeName.substring(0, index); 505 else 506 break; 507 } 508 509 // 510 // If we got this far, we failed. Remember for later. 511 // 512 cache.put(localeName, Boolean.FALSE); 513 return false; 514 } 515 }