1 /* 2 * $Id: PropertyMessageResources.java 480549 2006-11-29 12:16:15Z niallp $ 3 * 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 package org.apache.struts.util; 22 23 import org.apache.commons.logging.Log; 24 import org.apache.commons.logging.LogFactory; 25 26 import java.io.IOException; 27 import java.io.InputStream; 28 29 import java.util.HashMap; 30 import java.util.Iterator; 31 import java.util.Locale; 32 import java.util.Properties; 33 34 /** 35 * Concrete subclass of <code>MessageResources</code> that reads message keys 36 * and corresponding strings from named property resources in a <b><i>similar</i></b> manner 37 * (see <i>modes</i> below) that <code>java.util.PropertyResourceBundle</code> does. The 38 * <code>base</code> property defines the base property resource name, and 39 * must be specified. <p> <strong>IMPLEMENTATION NOTE</strong> - This class 40 * trades memory for speed by caching all messages located via generalizing 41 * the Locale under the original locale as well. This results in specific 42 * messages being stored in the message cache more than once, but improves 43 * response time on subsequent requests for the same locale + key 44 * combination. 45 * 46 * <h2>Operating Modes</h2> 47 * This implementation can be configured to operate in one of three modes: 48 * <ul> 49 * <li>1. <b>Default</b> - default, backwardly compatible, Struts behaviour (i.e. the way 50 * its always worked).</li> 51 * <li>2. <b>JSTL</b> - compatible with how JSTL finds messages 52 * (fix for <a href="http://issues.apache.org/struts/browse/STR-2925">STR-2925</a>)</li> 53 * <li>3. <b>Resource</b> - compatible with how Java's <code>PropertyResourceBundle</code> 54 * finds messages (fix for 55 * <a href="http://issues.apache.org/struts/browse/STR-2077">STR-2077</a>)</li> 56 * </ul> 57 * 58 * <h3>1. Default Mode</h3> 59 * <i>Default mode</i> is the way this implementation has always operated. It searches 60 * for a message key for property resources in the following sequence: 61 * <pre> 62 * base + "_" + localeLanguage + "_" + localeCountry + "_" + localeVariant 63 * base + "_" + localeLanguage + "_" + localeCountry 64 * base + "_" + localeLanguage 65 * base + "_" + default locale 66 * base 67 * </pre> 68 * <p> 69 * This mode is the <i>default</i> and requires no additional configuration. 70 * 71 * <h3>2. JSTL Mode</h3> 72 * <i>JSTL mode</i> is compatible with how JSTL operates and the default Locale 73 * is not used when looking for a message key. <i>JSTL mode</i> searches for 74 * a message key for property resources in the following sequence: 75 * <pre> 76 * base + "_" + localeLanguage + "_" + localeCountry + "_" + localeVariant 77 * base + "_" + localeLanguage + "_" + localeCountry 78 * base + "_" + localeLanguage 79 * base 80 * </pre> 81 * <p> 82 * Configure <code>PropertyMessageResources</code> to operate in this mode by 83 * specifying a value of <code>JSTL</code> for the <code>mode</code> 84 * key in your <code>struts-config.xml</code>: 85 * <pre> 86 * <message-resources parameter="mypackage.MyMessageResources"> 87 * <set-property key="mode" value="JSTL"/> 88 * </message-resources> 89 * </pre> 90 * 91 * <h3>3. Resource Mode</h3> 92 * <i>Resource mode</i> is compatible with how Java's <code>PropertyResourceBundle</code> 93 * operates. <i>Resource mode</i> searches first through the specified Locale's language, 94 * country and variant, then through the default Locale's language, 95 * country and variant and finally using just the <code>base</code>: 96 * <pre> 97 * base + "_" + localeLanguage + "_" + localeCountry + "_" + localeVariant 98 * base + "_" + localeLanguage + "_" + localeCountry 99 * base + "_" + localeLanguage 100 * base + "_" + defaultLanguage + "_" + defaultCountry + "_" + defaultVariant 101 * base + "_" + defaultLanguage + "_" + defaultCountry 102 * base + "_" + defaultLanguage 103 * base 104 * </pre> 105 * <p> 106 * Configure <code>PropertyMessageResources</code> to operate in this mode by 107 * specifying a value of <code>resource</code> for the <code>mode</code> 108 * key in your <code>struts-config.xml</code>: 109 * <pre> 110 * <message-resources parameter="mypackage.MyMessageResources"> 111 * <set-property key="mode" value="resource"/> 112 * </message-resources> 113 * </pre> 114 * 115 * @version $Rev: 480549 $ $Date: 2006-11-29 06:16:15 -0600 (Wed, 29 Nov 2006) $ 116 */ 117 public class PropertyMessageResources extends MessageResources { 118 119 120 /** Indicates compatibility with how PropertyMessageResources has always looked up messages */ 121 private static final int MODE_DEFAULT = 0; 122 123 /** Indicates compatibility with how JSTL looks up messages */ 124 private static final int MODE_JSTL = 1; 125 126 /** Indicates compatibility with how java's PropertyResourceBundle looks up messages */ 127 private static final int MODE_RESOURCE_BUNDLE = 2; 128 129 /** 130 * The <code>Log</code> instance for this class. 131 */ 132 protected static final Log log = 133 LogFactory.getLog(PropertyMessageResources.class); 134 135 // ------------------------------------------------------------- Properties 136 137 /** 138 * The set of locale keys for which we have already loaded messages, keyed 139 * by the value calculated in <code>localeKey()</code>. 140 */ 141 protected HashMap locales = new HashMap(); 142 143 /** 144 * The cache of messages we have accumulated over time, keyed by the value 145 * calculated in <code>messageKey()</code>. 146 */ 147 protected HashMap messages = new HashMap(); 148 149 /** 150 * Compatibility mode that PropertyMessageResources is operating in. 151 */ 152 private int mode = MODE_DEFAULT; 153 154 // ----------------------------------------------------------- Constructors 155 156 /** 157 * Construct a new PropertyMessageResources according to the specified 158 * parameters. 159 * 160 * @param factory The MessageResourcesFactory that created us 161 * @param config The configuration parameter for this MessageResources 162 */ 163 public PropertyMessageResources(MessageResourcesFactory factory, 164 String config) { 165 super(factory, config); 166 log.trace("Initializing, config='" + config + "'"); 167 } 168 169 /** 170 * Construct a new PropertyMessageResources according to the specified 171 * parameters. 172 * 173 * @param factory The MessageResourcesFactory that created us 174 * @param config The configuration parameter for this 175 * MessageResources 176 * @param returnNull The returnNull property we should initialize with 177 */ 178 public PropertyMessageResources(MessageResourcesFactory factory, 179 String config, boolean returnNull) { 180 super(factory, config, returnNull); 181 log.trace("Initializing, config='" + config + "', returnNull=" 182 + returnNull); 183 } 184 185 // --------------------------------------------------------- Public Methods 186 187 /** 188 * Set the compatibility mode this implementation uses for message lookup. 189 * 190 * @param mode <code>JSTL</code> for JSTL compatibility, 191 * <code>resource</code> for PropertyResourceBundle compatibility or 192 * <code>default</code> for Struts backward compatibility. 193 */ 194 public void setMode(String mode) { 195 String value = (mode == null ? null : mode.trim()); 196 if ("jstl".equalsIgnoreCase(value)) { 197 this.mode = MODE_JSTL; 198 if (log.isDebugEnabled()) { 199 log.info("Operating in JSTL compatible mode [" + mode + "]"); 200 } 201 } else if ("resource".equalsIgnoreCase(value)) { 202 this.mode = MODE_RESOURCE_BUNDLE; 203 if (log.isDebugEnabled()) { 204 log.info("Operating in PropertyResourceBundle compatible mode [" + mode + "]"); 205 } 206 } else { 207 this.mode = MODE_DEFAULT; 208 if (log.isDebugEnabled()) { 209 log.info("Operating in Default mode [" + mode + "]"); 210 } 211 } 212 } 213 214 /** 215 * Returns a text message for the specified key, for the specified or default 216 * Locale. A null string result will be returned by this method if no relevant 217 * message resource is found for this key or Locale, if the 218 * <code>returnNull</code> property is set. Otherwise, an appropriate 219 * error message will be returned. <p> This method must be implemented by 220 * a concrete subclass. 221 * 222 * @param locale The requested message Locale, or <code>null</code> for 223 * the system default Locale 224 * @param key The message key to look up 225 * @return text message for the specified key and locale 226 */ 227 public String getMessage(Locale locale, String key) { 228 if (log.isDebugEnabled()) { 229 log.debug("getMessage(" + locale + "," + key + ")"); 230 } 231 232 // Initialize variables we will require 233 String localeKey = localeKey(locale); 234 String originalKey = messageKey(localeKey, key); 235 String message = null; 236 237 // Search the specified Locale 238 message = findMessage(locale, key, originalKey); 239 if (message != null) { 240 return message; 241 } 242 243 // JSTL Compatibility - JSTL doesn't use the default locale 244 if (mode == MODE_JSTL) { 245 246 // do nothing (i.e. don't use default Locale) 247 248 // PropertyResourcesBundle - searches through the hierarchy 249 // for the default Locale (e.g. first en_US then en) 250 } else if (mode == MODE_RESOURCE_BUNDLE) { 251 252 if (!defaultLocale.equals(locale)) { 253 message = findMessage(defaultLocale, key, originalKey); 254 } 255 256 // Default (backwards) Compatibility - just searches the 257 // specified Locale (e.g. just en_US) 258 } else { 259 260 if (!defaultLocale.equals(locale)) { 261 localeKey = localeKey(defaultLocale); 262 message = findMessage(localeKey, key, originalKey); 263 } 264 265 } 266 if (message != null) { 267 return message; 268 } 269 270 // Find the message in the default properties file 271 message = findMessage("", key, originalKey); 272 if (message != null) { 273 return message; 274 } 275 276 // Return an appropriate error indication 277 if (returnNull) { 278 return (null); 279 } else { 280 return ("???" + messageKey(locale, key) + "???"); 281 } 282 } 283 284 // ------------------------------------------------------ Protected Methods 285 286 /** 287 * Load the messages associated with the specified Locale key. For this 288 * implementation, the <code>config</code> property should contain a fully 289 * qualified package and resource name, separated by periods, of a series 290 * of property resources to be loaded from the class loader that created 291 * this PropertyMessageResources instance. This is exactly the same name 292 * format you would use when utilizing the <code>java.util.PropertyResourceBundle</code> 293 * class. 294 * 295 * @param localeKey Locale key for the messages to be retrieved 296 */ 297 protected synchronized void loadLocale(String localeKey) { 298 if (log.isTraceEnabled()) { 299 log.trace("loadLocale(" + localeKey + ")"); 300 } 301 302 // Have we already attempted to load messages for this locale? 303 if (locales.get(localeKey) != null) { 304 return; 305 } 306 307 locales.put(localeKey, localeKey); 308 309 // Set up to load the property resource for this locale key, if we can 310 String name = config.replace('.', '/'); 311 312 if (localeKey.length() > 0) { 313 name += ("_" + localeKey); 314 } 315 316 name += ".properties"; 317 318 InputStream is = null; 319 Properties props = new Properties(); 320 321 // Load the specified property resource 322 if (log.isTraceEnabled()) { 323 log.trace(" Loading resource '" + name + "'"); 324 } 325 326 ClassLoader classLoader = 327 Thread.currentThread().getContextClassLoader(); 328 329 if (classLoader == null) { 330 classLoader = this.getClass().getClassLoader(); 331 } 332 333 is = classLoader.getResourceAsStream(name); 334 335 if (is != null) { 336 try { 337 props.load(is); 338 } catch (IOException e) { 339 log.error("loadLocale()", e); 340 } finally { 341 try { 342 is.close(); 343 } catch (IOException e) { 344 log.error("loadLocale()", e); 345 } 346 } 347 if (log.isTraceEnabled()) { 348 log.trace(" Loading resource completed"); 349 } 350 } else { 351 if (log.isWarnEnabled()) { 352 log.warn(" Resource "+name+" Not Found."); 353 } 354 } 355 356 357 // Copy the corresponding values into our cache 358 if (props.size() < 1) { 359 return; 360 } 361 362 synchronized (messages) { 363 Iterator names = props.keySet().iterator(); 364 365 while (names.hasNext()) { 366 String key = (String) names.next(); 367 368 if (log.isTraceEnabled()) { 369 log.trace(" Saving message key '" 370 + messageKey(localeKey, key)); 371 } 372 373 messages.put(messageKey(localeKey, key), props.getProperty(key)); 374 } 375 } 376 } 377 378 // -------------------------------------------------------- Private Methods 379 380 /** 381 * Returns a text message for the specified key, for the specified Locale. 382 * <p> 383 * A null string result will be returned by this method if no relevant 384 * message resource is found. This method searches through the locale 385 * <i>hierarchy</i> (i.e. variant --> languge --> country) for the message. 386 * 387 * @param locale The requested message Locale, or <code>null</code> for 388 * the system default Locale 389 * @param key The message key to look up 390 * @param originalKey The original message key to cache any found message under 391 * @return text message for the specified key and locale 392 */ 393 private String findMessage(Locale locale, String key, String originalKey) { 394 395 // Initialize variables we will require 396 String localeKey = localeKey(locale); 397 String messageKey = null; 398 String message = null; 399 int underscore = 0; 400 401 // Loop from specific to general Locales looking for this message 402 while (true) { 403 message = findMessage(localeKey, key, originalKey); 404 if (message != null) { 405 break; 406 } 407 408 // Strip trailing modifiers to try a more general locale key 409 underscore = localeKey.lastIndexOf("_"); 410 411 if (underscore < 0) { 412 break; 413 } 414 415 localeKey = localeKey.substring(0, underscore); 416 } 417 418 return message; 419 420 } 421 422 /** 423 * Returns a text message for the specified key, for the specified Locale. 424 * <p> 425 * A null string result will be returned by this method if no relevant 426 * message resource is found. 427 * 428 * @param locale The requested key of the Locale 429 * @param key The message key to look up 430 * @param originalKey The original message key to cache any found message under 431 * @return text message for the specified key and locale 432 */ 433 private String findMessage(String localeKey, String key, String originalKey) { 434 435 // Load this Locale's messages if we have not done so yet 436 loadLocale(localeKey); 437 438 // Check if we have this key for the current locale key 439 String messageKey = messageKey(localeKey, key); 440 441 // Add if not found under the original key 442 boolean addIt = !messageKey.equals(originalKey); 443 444 synchronized (messages) { 445 String message = (String) messages.get(messageKey); 446 447 if (message != null) { 448 if (addIt) { 449 messages.put(originalKey, message); 450 } 451 452 } 453 return (message); 454 } 455 } 456 }