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 }