View Javadoc

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