View Javadoc

1   package org.apache.turbine.services.localization;
2   
3   /*
4    * Copyright 2001-2005 The Apache Software Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License")
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import java.util.HashMap;
20  import java.util.Locale;
21  import java.util.Map;
22  import java.util.MissingResourceException;
23  import java.util.ResourceBundle;
24  
25  import javax.servlet.http.HttpServletRequest;
26  
27  import org.apache.commons.configuration.Configuration;
28  
29  import org.apache.commons.lang.StringUtils;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  
34  import org.apache.turbine.Turbine;
35  import org.apache.turbine.services.InitializationException;
36  import org.apache.turbine.services.TurbineBaseService;
37  import org.apache.turbine.util.RunData;
38  
39  /***
40   * <p>This class is the single point of access to all localization
41   * resources.  It caches different ResourceBundles for different
42   * Locales.</p>
43   *
44   * <p>Usage example:</p>
45   *
46   * <blockquote><code><pre>
47   * LocalizationService ls = (LocalizationService) TurbineServices
48   *     .getInstance().getService(LocalizationService.SERVICE_NAME);
49   * </pre></code></blockquote>
50   *
51   * <p>Then call one of four methods to retrieve a ResourceBundle:
52   *
53   * <ul>
54   * <li>getBundle("MyBundleName")</li>
55   * <li>getBundle("MyBundleName", httpAcceptLanguageHeader)</li>
56   * <li>etBundle("MyBundleName", HttpServletRequest)</li>
57   * <li>getBundle("MyBundleName", Locale)</li>
58   * <li>etc.</li>
59   * </ul></p>
60   *
61   * @author <a href="mailto:jm@mediaphil.de">Jonas Maurus</a>
62   * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
63   * @author <a href="mailto:novalidemail@foo.com">Frank Y. Kim</a>
64   * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
65   * @author <a href="mailto:leonardr@collab.net">Leonard Richardson</a>
66   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
67   * @version $Id: TurbineLocalizationService.java 264152 2005-08-29 14:50:22Z henning $
68   */
69  public class TurbineLocalizationService
70          extends TurbineBaseService
71          implements LocalizationService
72  {
73      /*** Logging */
74      private static Log log = LogFactory.getLog(TurbineLocalizationService.class);
75  
76      /***
77       * The value to pass to <code>MessageFormat</code> if a
78       * <code>null</code> reference is passed to <code>format()</code>.
79       */
80      private static final Object[] NO_ARGS = new Object[0];
81  
82      /***
83       * Bundle name keys a Map of the ResourceBundles in this
84       * service (which is in turn keyed by Locale).
85       * Key=bundle name
86       * Value=Hashtable containing ResourceBundles keyed by Locale.
87       */
88      private Map bundles = null;
89  
90      /***
91       * The list of default bundles to search.
92       */
93      private String[] bundleNames = null;
94  
95      /***
96       * The name of the default locale to use (includes language and
97       * country).
98       */
99      private Locale defaultLocale = null;
100 
101     /*** The name of the default language to use. */
102     private String defaultLanguage = null;
103 
104     /*** The name of the default country to use. */
105     private String defaultCountry = null;
106 
107     /***
108      * Constructor.
109      */
110     public TurbineLocalizationService()
111     {
112         bundles = new HashMap();
113     }
114 
115     /***
116      * Called the first time the Service is used.
117      */
118     public void init()
119             throws InitializationException
120     {
121         Configuration conf = Turbine.getConfiguration();
122 
123         initBundleNames(null);
124 
125         Locale jvmDefault = Locale.getDefault();
126 
127         defaultLanguage = conf.getString("locale.default.language",
128                 jvmDefault.getLanguage()).trim();
129         defaultCountry = conf.getString("locale.default.country",
130                 jvmDefault.getCountry()).trim();
131 
132         defaultLocale = new Locale(defaultLanguage, defaultCountry);
133         setInit(true);
134     }
135 
136     /***
137      * Initialize list of default bundle names.
138      *
139      * @param ignored Ignored.
140      */
141     protected void initBundleNames(String[] ignored)
142     {
143         Configuration conf = Turbine.getConfiguration();
144         bundleNames = conf.getStringArray("locale.default.bundles");
145         String name = conf.getString("locale.default.bundle");
146 
147         if (name != null && name.length() > 0)
148         {
149             // Using old-style single bundle name property.
150             if (bundleNames == null || bundleNames.length <= 0)
151             {
152                 bundleNames = new String[] {name};
153             }
154             else
155             {
156                 // Prepend "default" bundle name.
157                 String[] array = new String[bundleNames.length + 1];
158                 array[0] = name;
159                 System.arraycopy(bundleNames, 0, array, 1, bundleNames.length);
160                 bundleNames = array;
161             }
162         }
163         if (bundleNames == null)
164         {
165             bundleNames = new String[0];
166         }
167     }
168 
169     /***
170      * Retrieves the default language (specified in the config file).
171      */
172     public String getDefaultLanguage()
173     {
174         return defaultLanguage;
175     }
176 
177     /***
178      * Retrieves the default country (specified in the config file).
179      */
180     public String getDefaultCountry()
181     {
182         return defaultCountry;
183     }
184 
185     /***
186      * Retrieves the name of the default bundle (as specified in the
187      * config file).
188      * @see org.apache.turbine.services.localization.LocalizationService#getDefaultBundleName()
189      */
190     public String getDefaultBundleName()
191     {
192         return (bundleNames.length > 0 ? bundleNames[0] : "");
193     }
194 
195     /***
196      * @see org.apache.turbine.services.localization.LocalizationService#getBundleNames()
197      */
198     public String[] getBundleNames()
199     {
200         return (String []) bundleNames.clone();
201     }
202 
203     /***
204      * This method returns a ResourceBundle given the bundle name
205      * "DEFAULT" and the default Locale information supplied in
206      * TurbineProperties.
207      *
208      * @return A localized ResourceBundle.
209      */
210     public ResourceBundle getBundle()
211     {
212         return getBundle(getDefaultBundleName(), (Locale) null);
213     }
214 
215     /***
216      * This method returns a ResourceBundle given the bundle name and
217      * the default Locale information supplied in TurbineProperties.
218      *
219      * @param bundleName Name of bundle.
220      * @return A localized ResourceBundle.
221      */
222     public ResourceBundle getBundle(String bundleName)
223     {
224         return getBundle(bundleName, (Locale) null);
225     }
226 
227     /***
228      * This method returns a ResourceBundle given the bundle name and
229      * the Locale information supplied in the HTTP "Accept-Language"
230      * header.
231      *
232      * @param bundleName Name of bundle.
233      * @param languageHeader A String with the language header.
234      * @return A localized ResourceBundle.
235      */
236     public ResourceBundle getBundle(String bundleName, String languageHeader)
237     {
238         return getBundle(bundleName, getLocale(languageHeader));
239     }
240 
241     /***
242      * This method returns a ResourceBundle given the Locale
243      * information supplied in the HTTP "Accept-Language" header which
244      * is stored in HttpServletRequest.
245      *
246      * @param req HttpServletRequest.
247      * @return A localized ResourceBundle.
248      */
249     public ResourceBundle getBundle(HttpServletRequest req)
250     {
251         return getBundle(getDefaultBundleName(), getLocale(req));
252     }
253 
254     /***
255      * This method returns a ResourceBundle given the bundle name and
256      * the Locale information supplied in the HTTP "Accept-Language"
257      * header which is stored in HttpServletRequest.
258      *
259      * @param bundleName Name of the bundle to use if the request's
260      * locale cannot be resolved.
261      * @param req HttpServletRequest.
262      * @return A localized ResourceBundle.
263      */
264     public ResourceBundle getBundle(String bundleName, HttpServletRequest req)
265     {
266         return getBundle(bundleName, getLocale(req));
267     }
268 
269     /***
270      * This method returns a ResourceBundle given the Locale
271      * information supplied in the HTTP "Accept-Language" header which
272      * is stored in RunData.
273      *
274      * @param data Turbine information.
275      * @return A localized ResourceBundle.
276      */
277     public ResourceBundle getBundle(RunData data)
278     {
279         return getBundle(getDefaultBundleName(), getLocale(data.getRequest()));
280     }
281 
282     /***
283      * This method returns a ResourceBundle given the bundle name and
284      * the Locale information supplied in the HTTP "Accept-Language"
285      * header which is stored in RunData.
286      *
287      * @param bundleName Name of bundle.
288      * @param data Turbine information.
289      * @return A localized ResourceBundle.
290      */
291     public ResourceBundle getBundle(String bundleName, RunData data)
292     {
293         return getBundle(bundleName, getLocale(data.getRequest()));
294     }
295 
296     /***
297      * This method returns a ResourceBundle for the given bundle name
298      * and the given Locale.
299      *
300      * @param bundleName Name of bundle (or <code>null</code> for the
301      * default bundle).
302      * @param locale The locale (or <code>null</code> for the locale
303      * indicated by the default language and country).
304      * @return A localized ResourceBundle.
305      */
306     public ResourceBundle getBundle(String bundleName, Locale locale)
307     {
308         // Assure usable inputs.
309         bundleName = (StringUtils.isEmpty(bundleName) ? getDefaultBundleName() : bundleName.trim());
310         if (locale == null)
311         {
312             locale = getLocale((String) null);
313         }
314 
315         // Find/retrieve/cache bundle.
316         ResourceBundle rb = null;
317         Map bundlesByLocale = (Map) bundles.get(bundleName);
318         if (bundlesByLocale != null)
319         {
320             // Cache of bundles by locale for the named bundle exists.
321             // Check the cache for a bundle corresponding to locale.
322             rb = (ResourceBundle) bundlesByLocale.get(locale);
323 
324             if (rb == null)
325             {
326                 // Not yet cached.
327                 rb = cacheBundle(bundleName, locale);
328             }
329         }
330         else
331         {
332             rb = cacheBundle(bundleName, locale);
333         }
334         return rb;
335     }
336 
337     /***
338      * Caches the named bundle for fast lookups.  This operation is
339      * relatively expesive in terms of memory use, but is optimized
340      * for run-time speed in the usual case.
341      *
342      * @exception MissingResourceException Bundle not found.
343      */
344     private synchronized ResourceBundle cacheBundle(String bundleName,
345                                                     Locale locale)
346         throws MissingResourceException
347     {
348         Map bundlesByLocale = (HashMap) bundles.get(bundleName);
349         ResourceBundle rb = (bundlesByLocale == null ? null :
350                              (ResourceBundle) bundlesByLocale.get(locale));
351 
352         if (rb == null)
353         {
354             bundlesByLocale = (bundlesByLocale == null ? new HashMap(3) :
355                                new HashMap(bundlesByLocale));
356             try
357             {
358                 rb = ResourceBundle.getBundle(bundleName, locale);
359             }
360             catch (MissingResourceException e)
361             {
362                 rb = findBundleByLocale(bundleName, locale, bundlesByLocale);
363                 if (rb == null)
364                 {
365                     throw (MissingResourceException) e.fillInStackTrace();
366                 }
367             }
368 
369             if (rb != null)
370             {
371                 // Cache bundle.
372                 bundlesByLocale.put(rb.getLocale(), rb);
373 
374                 Map bundlesByName = new HashMap(bundles);
375                 bundlesByName.put(bundleName, bundlesByLocale);
376                 this.bundles = bundlesByName;
377             }
378         }
379         return rb;
380     }
381 
382     /***
383      * <p>Retrieves the bundle most closely matching first against the
384      * supplied inputs, then against the defaults.</p>
385      *
386      * <p>Use case: some clients send a HTTP Accept-Language header
387      * with a value of only the language to use
388      * (i.e. "Accept-Language: en"), and neglect to include a country.
389      * When there is no bundle for the requested language, this method
390      * can be called to try the default country (checking internally
391      * to assure the requested criteria matches the default to avoid
392      * disconnects between language and country).</p>
393      *
394      * <p>Since we're really just guessing at possible bundles to use,
395      * we don't ever throw <code>MissingResourceException</code>.</p>
396      */
397     private ResourceBundle findBundleByLocale(String bundleName, Locale locale,
398                                               Map bundlesByLocale)
399     {
400         ResourceBundle rb = null;
401         if (!StringUtils.isNotEmpty(locale.getCountry()) &&
402              defaultLanguage.equals(locale.getLanguage()))
403         {
404             /*
405              *              log.debug("Requested language '" + locale.getLanguage() +
406              *              "' matches default: Attempting to guess bundle " +
407              *              "using default country '" + defaultCountry + '\'');
408              */
409             Locale withDefaultCountry = new Locale(locale.getLanguage(),
410                                                    defaultCountry);
411             rb = (ResourceBundle) bundlesByLocale.get(withDefaultCountry);
412             if (rb == null)
413             {
414                 rb = getBundleIgnoreException(bundleName, withDefaultCountry);
415             }
416         }
417         else if (!StringUtils.isNotEmpty(locale.getLanguage()) &&
418                   defaultCountry.equals(locale.getCountry()))
419         {
420             Locale withDefaultLanguage = new Locale(defaultLanguage,
421                                                     locale.getCountry());
422             rb = (ResourceBundle) bundlesByLocale.get(withDefaultLanguage);
423             if (rb == null)
424             {
425                 rb = getBundleIgnoreException(bundleName, withDefaultLanguage);
426             }
427         }
428 
429         if (rb == null && !defaultLocale.equals(locale))
430         {
431             rb = getBundleIgnoreException(bundleName, defaultLocale);
432         }
433 
434         return rb;
435     }
436 
437     /***
438      * Retrieves the bundle using the
439      * <code>ResourceBundle.getBundle(String, Locale)</code> method,
440      * returning <code>null</code> instead of throwing
441      * <code>MissingResourceException</code>.
442      */
443     private ResourceBundle getBundleIgnoreException(String bundleName,
444                                                           Locale locale)
445     {
446         try
447         {
448             return ResourceBundle.getBundle(bundleName, locale);
449         }
450         catch (MissingResourceException ignored)
451         {
452             return null;
453         }
454     }
455 
456     /***
457      * This method sets the name of the first bundle in the search
458      * list (the "default" bundle).
459      *
460      * @param defaultBundle Name of default bundle.
461      */
462     public void setBundle(String defaultBundle)
463     {
464         if (bundleNames.length > 0)
465         {
466             bundleNames[0] = defaultBundle;
467         }
468         else
469         {
470             synchronized (this)
471             {
472                 if (bundleNames.length <= 0)
473                 {
474                     bundleNames = new String[] {defaultBundle};
475                 }
476             }
477         }
478     }
479 
480     /***
481      * @see org.apache.turbine.services.localization.LocalizationService#getLocale(HttpServletRequest)
482      */
483     public final Locale getLocale(HttpServletRequest req)
484     {
485         return getLocale(req.getHeader(ACCEPT_LANGUAGE));
486     }
487 
488     /***
489      * @see org.apache.turbine.services.localization.LocalizationService#getLocale(String)
490      */
491     public Locale getLocale(String header)
492     {
493         if (!StringUtils.isEmpty(header))
494         {
495             LocaleTokenizer tok = new LocaleTokenizer(header);
496             if (tok.hasNext())
497             {
498                 return (Locale) tok.next();
499             }
500         }
501 
502         // Couldn't parse locale.
503         return defaultLocale;
504     }
505 
506     /***
507      * @exception MissingResourceException Specified key cannot be matched.
508      * @see org.apache.turbine.services.localization.LocalizationService#getString(String, Locale, String)
509      */
510     public String getString(String bundleName, Locale locale, String key)
511     {
512         String value = null;
513 
514         if (locale == null)
515         {
516             locale = getLocale((String) null);
517         }
518 
519         // Look for text in requested bundle.
520         ResourceBundle rb = getBundle(bundleName, locale);
521         value = getStringOrNull(rb, key);
522 
523         // Look for text in list of default bundles.
524         if (value == null && bundleNames.length > 0)
525         {
526             String name;
527             for (int i = 0; i < bundleNames.length; i++)
528             {
529                 name = bundleNames[i];
530                 //System.out.println("getString(): name=" + name +
531                 //                   ", locale=" + locale + ", i=" + i);
532                 if (!name.equals(bundleName))
533                 {
534                     rb = getBundle(name, locale);
535                     value = getStringOrNull(rb, key);
536                     if (value != null)
537                     {
538                         locale = rb.getLocale();
539                         break;
540                     }
541                 }
542             }
543         }
544 
545         if (value == null)
546         {
547             String loc = locale.toString();
548             String mesg = LocalizationService.SERVICE_NAME +
549                 " noticed missing resource: " +
550                 "bundleName=" + bundleName + ", locale=" + loc +
551                 ", key=" + key;
552             log.debug(mesg);
553             // Text not found in requested or default bundles.
554             throw new MissingResourceException(mesg, bundleName, key);
555         }
556 
557         return value;
558     }
559 
560     /***
561      * Gets localized text from a bundle if it's there.  Otherwise,
562      * returns <code>null</code> (ignoring a possible
563      * <code>MissingResourceException</code>).
564      */
565     protected final String getStringOrNull(ResourceBundle rb, String key)
566     {
567         if (rb != null)
568         {
569             try
570             {
571                 return rb.getString(key);
572             }
573             catch (MissingResourceException ignored)
574             {
575             }
576         }
577         return null;
578     }
579 
580 }