1 package org.apache.turbine.services.localization;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
154 if (bundleNames == null || bundleNames.length <= 0)
155 {
156 bundleNames = new String[] {name};
157 }
158 else
159 {
160
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
313 bundleName = (StringUtils.isEmpty(bundleName) ? getDefaultBundleName() : bundleName.trim());
314 if (locale == null)
315 {
316 locale = getLocale((String) null);
317 }
318
319
320 ResourceBundle rb = null;
321 Map bundlesByLocale = (Map) bundles.get(bundleName);
322 if (bundlesByLocale != null)
323 {
324
325
326 rb = (ResourceBundle) bundlesByLocale.get(locale);
327
328 if (rb == null)
329 {
330
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
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
410
411
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
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
524 ResourceBundle rb = getBundle(bundleName, locale);
525 value = getStringOrNull(rb, key);
526
527
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
535
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
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
637
638 locale = getLocale((String) null);
639 }
640 String value = getString(bundleName, locale, key);
641 if (args == null)
642 {
643 args = NO_ARGS;
644 }
645
646
647
648
649 MessageFormat messageFormat = new MessageFormat("");
650 messageFormat.setLocale(locale);
651 messageFormat.applyPattern(value);
652 return messageFormat.format(args);
653 }
654 }