1 /*
2  *  Licensed to the Apache Software Foundation (ASF) under one or more
3  *  contributor license agreements.  See the NOTICE file distributed with
4  *  this work for additional information regarding copyright ownership.
5  *  The ASF licenses this file to You under the Apache License, Version 2.0
6  *  (the "License"); you may not use this file except in compliance with
7  *  the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */

17 package org.apache.tomcat.util.res;
18
19 import java.text.MessageFormat;
20 import java.util.Enumeration;
21 import java.util.Hashtable;
22 import java.util.LinkedHashMap;
23 import java.util.Locale;
24 import java.util.Map;
25 import java.util.MissingResourceException;
26 import java.util.ResourceBundle;
27
28 /**
29  * An internationalization / localization helper class which reduces
30  * the bother of handling ResourceBundles and takes care of the
31  * common cases of message formatting which otherwise require the
32  * creation of Object arrays and such.
33  *
34  * <p>The StringManager operates on a package basis. One StringManager
35  * per package can be created and accessed via the getManager method
36  * call.
37  *
38  * <p>The StringManager will look for a ResourceBundle named by
39  * the package name given plus the suffix of "LocalStrings". In
40  * practice, this means that the localized information will be contained
41  * in a LocalStrings.properties file located in the package
42  * directory of the class path.
43  *
44  * <p>Please see the documentation for java.util.ResourceBundle for
45  * more information.
46  *
47  * @author James Duncan Davidson [duncan@eng.sun.com]
48  * @author James Todd [gonzo@eng.sun.com]
49  * @author Mel Martinez [mmartinez@g1440.com]
50  * @see java.util.ResourceBundle
51  */

52 public class StringManager {
53
54     private static int LOCALE_CACHE_SIZE = 10;
55
56     /**
57      * The ResourceBundle for this StringManager.
58      */

59     private final ResourceBundle bundle;
60     private final Locale locale;
61
62
63     /**
64      * Creates a new StringManager for a given package. This is a
65      * private method and all access to it is arbitrated by the
66      * static getManager method call so that only one StringManager
67      * per package will be created.
68      *
69      * @param packageName Name of package to create StringManager for.
70      */

71     private StringManager(String packageName, Locale locale) {
72         String bundleName = packageName + ".LocalStrings";
73         ResourceBundle bnd = null;
74         try {
75             // The ROOT Locale uses English. If English is requested, force the
76             // use of the ROOT Locale else incorrect results may be obtained if
77             // the system default locale is not English and translations are
78             // available for the system default locale.
79             if (locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
80                 locale = Locale.ROOT;
81             }
82             bnd = ResourceBundle.getBundle(bundleName, locale);
83         } catch (MissingResourceException ex) {
84             // Try from the current loader (that's the case for trusted apps)
85             // Should only be required if using a TC5 style classloader structure
86             // where common != shared != server
87             ClassLoader cl = Thread.currentThread().getContextClassLoader();
88             if (cl != null) {
89                 try {
90                     bnd = ResourceBundle.getBundle(bundleName, locale, cl);
91                 } catch (MissingResourceException ex2) {
92                     // Ignore
93                 }
94             }
95         }
96         bundle = bnd;
97         // Get the actual locale, which may be different from the requested one
98         if (bundle != null) {
99             Locale bundleLocale = bundle.getLocale();
100             if (bundleLocale.equals(Locale.ROOT)) {
101                 this.locale = Locale.ENGLISH;
102             } else {
103                 this.locale = bundleLocale;
104             }
105         } else {
106             this.locale = null;
107         }
108     }
109
110
111     /**
112      * Get a string from the underlying resource bundle or return null if the
113      * String is not found.
114      *
115      * @param key to desired resource String
116      *
117      * @return resource String matching <i>key</i> from underlying bundle or
118      *         null if not found.
119      *
120      * @throws IllegalArgumentException if <i>key</i> is null
121      */

122     public String getString(String key) {
123         if (key == null){
124             String msg = "key may not have a null value";
125             throw new IllegalArgumentException(msg);
126         }
127
128         String str = null;
129
130         try {
131             // Avoid NPE if bundle is null and treat it like an MRE
132             if (bundle != null) {
133                 str = bundle.getString(key);
134             }
135         } catch (MissingResourceException mre) {
136             //bad: shouldn't mask an exception the following way:
137             //   str = "[cannot find message associated with key '" + key +
138             //         "' due to " + mre + "]";
139             //     because it hides the fact that the String was missing
140             //     from the calling code.
141             //good: could just throw the exception (or wrap it in another)
142             //      but that would probably cause much havoc on existing
143             //      code.
144             //better: consistent with container pattern to
145             //      simply return null.  Calling code can then do
146             //      a null check.
147             str = null;
148         }
149
150         return str;
151     }
152
153
154     /**
155      * Get a string from the underlying resource bundle and format
156      * it with the given set of arguments.
157      *
158      * @param key  The key for the required message
159      * @param args The values to insert into the message
160      *
161      * @return The request string formatted with the provided arguments or the
162      *         key if the key was not found.
163      */

164     public String getString(final String key, final Object... args) {
165         String value = getString(key);
166         if (value == null) {
167             value = key;
168         }
169
170         MessageFormat mf = new MessageFormat(value);
171         mf.setLocale(locale);
172         return mf.format(args, new StringBuffer(), null).toString();
173     }
174
175
176     /**
177      * Identify the Locale this StringManager is associated with.
178      *
179      * @return The Locale associated with the StringManager
180      */

181     public Locale getLocale() {
182         return locale;
183     }
184
185
186     // --------------------------------------------------------------
187     // STATIC SUPPORT METHODS
188     // --------------------------------------------------------------
189
190     private static final Map<String, Map<Locale,StringManager>> managers =
191             new Hashtable<>();
192
193
194     /**
195      * Get the StringManager for a given class. The StringManager will be
196      * returned for the package in which the class is located. If a manager for
197      * that package already exists, it will be reused, else a new
198      * StringManager will be created and returned.
199      *
200      * @param clazz The class for which to retrieve the StringManager
201      *
202      * @return The instance associated with the package of the provide class
203      */

204     public static final StringManager getManager(Class<?> clazz) {
205         return getManager(clazz.getPackage().getName());
206     }
207
208
209     /**
210      * Get the StringManager for a particular package. If a manager for
211      * a package already exists, it will be reused, else a new
212      * StringManager will be created and returned.
213      *
214      * @param packageName The package name
215      *
216      * @return The instance associated with the given package and the default
217      *         Locale
218      */

219     public static final StringManager getManager(String packageName) {
220         return getManager(packageName, Locale.getDefault());
221     }
222
223
224     /**
225      * Get the StringManager for a particular package and Locale. If a manager
226      * for a package/Locale combination already exists, it will be reused, else
227      * a new StringManager will be created and returned.
228      *
229      * @param packageName The package name
230      * @param locale      The Locale
231      *
232      * @return The instance associated with the given package and Locale
233      */

234     public static final synchronized StringManager getManager(
235             String packageName, Locale locale) {
236
237         Map<Locale,StringManager> map = managers.get(packageName);
238         if (map == null) {
239             /*
240              * Don't want the HashMap to be expanded beyond LOCALE_CACHE_SIZE.
241              * Expansion occurs when size() exceeds capacity. Therefore keep
242              * size at or below capacity.
243              * removeEldestEntry() executes after insertion therefore the test
244              * for removal needs to use one less than the maximum desired size
245              *
246              */

247             map = new LinkedHashMap<Locale,StringManager>(LOCALE_CACHE_SIZE, 1, true) {
248                 private static final long serialVersionUID = 1L;
249                 @Override
250                 protected boolean removeEldestEntry(
251                         Map.Entry<Locale,StringManager> eldest) {
252                     if (size() > (LOCALE_CACHE_SIZE - 1)) {
253                         return true;
254                     }
255                     return false;
256                 }
257             };
258             managers.put(packageName, map);
259         }
260
261         StringManager mgr = map.get(locale);
262         if (mgr == null) {
263             mgr = new StringManager(packageName, locale);
264             map.put(locale, mgr);
265         }
266         return mgr;
267     }
268
269
270     /**
271      * Retrieve the StringManager for a list of Locales. The first StringManager
272      * found will be returned.
273      *
274      * @param packageName      The package for which the StringManager was
275      *                         requested
276      * @param requestedLocales The list of Locales
277      *
278      * @return the found StringManager or the default StringManager
279      */

280     public static StringManager getManager(String packageName,
281             Enumeration<Locale> requestedLocales) {
282         while (requestedLocales.hasMoreElements()) {
283             Locale locale = requestedLocales.nextElement();
284             StringManager result = getManager(packageName, locale);
285             if (result.getLocale().equals(locale)) {
286                 return result;
287             }
288         }
289         // Return the default
290         return getManager(packageName);
291     }
292 }
293