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 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
150 if (bundleNames == null || bundleNames.length <= 0)
151 {
152 bundleNames = new String[] {name};
153 }
154 else
155 {
156
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
309 bundleName = (StringUtils.isEmpty(bundleName) ? getDefaultBundleName() : bundleName.trim());
310 if (locale == null)
311 {
312 locale = getLocale((String) null);
313 }
314
315
316 ResourceBundle rb = null;
317 Map bundlesByLocale = (Map) bundles.get(bundleName);
318 if (bundlesByLocale != null)
319 {
320
321
322 rb = (ResourceBundle) bundlesByLocale.get(locale);
323
324 if (rb == null)
325 {
326
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
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
406
407
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
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
520 ResourceBundle rb = getBundle(bundleName, locale);
521 value = getStringOrNull(rb, key);
522
523
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
531
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
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 }