View Javadoc
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   *      &lt;message-resources parameter="mypackage.MyMessageResources"&gt;
85   *          &lt;set-property key="mode" value="JSTL"/&gt;
86   *      &lt;/message-resources&gt;
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  *      &lt;message-resources parameter="mypackage.MyMessageResources"&gt;
109  *          &lt;set-property key="mode" value="resource"/&gt;
110  *      &lt;/message-resources&gt;
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 }