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