001 package org.apache.fulcrum.localization;
002
003 /*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements. See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership. The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License. You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied. See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022 import java.text.MessageFormat;
023 import java.util.HashMap;
024 import java.util.Locale;
025 import java.util.Map;
026 import java.util.MissingResourceException;
027 import java.util.ResourceBundle;
028
029 import org.apache.avalon.framework.activity.Initializable;
030 import org.apache.avalon.framework.configuration.Configurable;
031 import org.apache.avalon.framework.configuration.Configuration;
032 import org.apache.avalon.framework.configuration.ConfigurationException;
033 import org.apache.avalon.framework.logger.AbstractLogEnabled;
034 import org.apache.commons.lang.StringUtils;
035
036 /**
037 * <p>This class is the single point of access to all localization
038 * resources. It caches different ResourceBundles for different
039 * Locales.</p>
040 *
041 * <p>Usage example:</p>
042 *
043 * <blockquote><code><pre>
044 * SimpleLocalizationService ls = (SimpleLocalizationService) TurbineServices
045 * .getInstance().getService(SimpleLocalizationService.SERVICE_NAME);
046 * </pre></code></blockquote>
047 *
048 * <p>Then call {@link #getString(String, Locale, String)}, or one of
049 * two methods to retrieve a ResourceBundle:
050 *
051 * <ul>
052 * <li>getBundle("MyBundleName")</li>
053 * <li>getBundle("MyBundleName", Locale)</li>
054 * <li>etc.</li>
055 * </ul></p>
056 *
057 * @author <a href="mailto:jm@mediaphil.de">Jonas Maurus</a>
058 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
059 * @author <a href="mailto:novalidemail@foo.com">Frank Y. Kim</a>
060 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
061 * @author <a href="mailto:leonardr@collab.net">Leonard Richardson</a>
062 * @author <a href="mailto:mcconnell@apache.org">Stephen McConnell</a>
063 * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
064 * @version $Id: DefaultLocalizationService.java 535465 2007-05-05 06:58:06Z tv $
065 * @avalon.component name="localization" lifestyle="singleton"
066 * @avalon.service type="org.apache.fulcrum.localization.SimpleLocalizationService"
067 */
068 public class SimpleLocalizationServiceImpl
069 extends AbstractLogEnabled
070 implements SimpleLocalizationService, Configurable, Initializable
071 {
072 /** Key Prefix for our bundles */
073 private static final String BUNDLES = "bundles";
074 /**
075 * The value to pass to <code>MessageFormat</code> if a
076 * <code>null</code> reference is passed to <code>format()</code>.
077 */
078 private static final Object[] NO_ARGS = new Object[0];
079 /**
080 * Bundle name keys a HashMap of the ResourceBundles in this
081 * service (which is in turn keyed by Locale).
082 */
083 private HashMap bundles = null;
084 /**
085 * The list of default bundles to search.
086 */
087 private String[] bundleNames = null;
088 /**
089 * The default bundle name to use if not specified.
090 */
091 private String defaultBundleName = null;
092 /**
093 * The name of the default locale to use (includes language and
094 * country).
095 */
096 private Locale defaultLocale = null;
097 /** The name of the default language to use. */
098 private String defaultLanguage;
099 /** The name of the default country to use. */
100 private String defaultCountry = null;
101
102 /**
103 * Creates a new instance.
104 */
105 public SimpleLocalizationServiceImpl()
106 {
107 bundles = new HashMap();
108 }
109
110 /**
111 * Avalon lifecycle method
112 *
113 * @see {@link Configurable}
114 */
115 public void configure(Configuration conf) throws ConfigurationException
116 {
117 Locale jvmDefault = Locale.getDefault();
118 defaultLanguage =
119 conf
120 .getAttribute(
121 "locale-default-language",
122 jvmDefault.getLanguage())
123 .trim();
124 defaultCountry =
125 conf
126 .getAttribute("locale-default-country", jvmDefault.getCountry())
127 .trim();
128 // FIXME! need to add bundle names
129 getLogger().info(
130 "initialized lang="
131 + defaultLanguage
132 + " country="
133 + defaultCountry);
134 final Configuration bundles = conf.getChild(BUNDLES, false);
135 if (bundles != null)
136 {
137 Configuration[] nameVal = bundles.getChildren();
138 String bundleName[] = new String[nameVal.length];
139 for (int i = 0; i < nameVal.length; i++)
140 {
141 String val = nameVal[i].getValue();
142 getLogger().debug("Registered bundle " + val);
143 bundleName[i] = val;
144 }
145 initBundleNames(bundleName);
146 }
147 }
148
149 /**
150 * Called the first time the Service is used.
151 */
152 public void initialize() throws Exception
153 {
154 // initBundleNames(null);
155 defaultLocale = new Locale(defaultLanguage, defaultCountry);
156 if (getLogger().isInfoEnabled())
157 {
158 getLogger().info("Localization Service is Initialized now..");
159 }
160 }
161
162 /**
163 * Initialize list of default bundle names.
164 *
165 * @param ignored names Ignored.
166 */
167 protected void initBundleNames(String[] intBundleNames)
168 {
169 //System.err.println("cfg=" + getConfiguration());
170 if (defaultBundleName != null && defaultBundleName.length() > 0)
171 {
172 // Using old-style single bundle name property.
173 if (intBundleNames == null || intBundleNames.length <= 0)
174 {
175 bundleNames = new String[] { defaultBundleName };
176 }
177 else
178 {
179 // Prepend "default" bundle name.
180 String[] array = new String[intBundleNames.length + 1];
181 array[0] = defaultBundleName;
182 System.arraycopy(
183 intBundleNames,
184 0,
185 array,
186 1,
187 intBundleNames.length);
188 bundleNames = array;
189 }
190 }
191 if (intBundleNames == null)
192 {
193 bundleNames = new String[0];
194 }
195 bundleNames = intBundleNames;
196 }
197
198 /**
199 * Retrieves the default language (specified in the config file).
200 */
201 public String getDefaultLanguage()
202 {
203 return defaultLanguage;
204 }
205
206 /**
207 * Retrieves the default country (specified in the config file).
208 */
209 public String getDefaultCountry()
210 {
211 return defaultCountry;
212 }
213
214 /**
215 * Retrieves the default Locale (as created from default
216 * language and default country).
217 */
218 public Locale getDefaultLocale()
219 {
220 return defaultLocale;
221 }
222
223 /**
224 * @see org.apache.fulcrum.localization.SimpleLocalizationService#getDefaultBundleName()
225 */
226 public String getDefaultBundleName()
227 {
228 return (bundleNames.length > 0 ? bundleNames[0] : "");
229 }
230
231 /**
232 * @see org.apache.fulcrum.localization.SimpleLocalizationService#getBundleNames()
233 */
234 public String[] getBundleNames()
235 {
236 return (String[]) bundleNames.clone();
237 }
238
239 /**
240 * @see org.apache.fulcrum.localization.SimpleLocalizationService#getBundle()
241 */
242 public ResourceBundle getBundle()
243 {
244 return getBundle(getDefaultBundleName(), (Locale) null);
245 }
246
247 /**
248 * @see org.apache.fulcrum.localization.SimpleLocalizationService#getBundle(String)
249 */
250 public ResourceBundle getBundle(String bundleName)
251 {
252 return getBundle(bundleName, (Locale) null);
253 }
254
255 /**
256 * This method returns a ResourceBundle for the given bundle name
257 * and the given Locale.
258 *
259 * @param bundleName Name of bundle (or <code>null</code> for the
260 * default bundle).
261 * @param locale The locale (or <code>null</code> for the locale
262 * indicated by the default language and country).
263 * @return A localized ResourceBundle.
264 */
265 public ResourceBundle getBundle(String bundleName, Locale locale)
266 {
267 // Assure usable inputs.
268 bundleName =
269 (bundleName == null ? getDefaultBundleName() : bundleName.trim());
270 if (locale == null)
271 {
272 locale = getDefaultLocale();
273 }
274 // Find/retrieve/cache bundle.
275 ResourceBundle rb = null;
276 HashMap bundlesByLocale = (HashMap) bundles.get(bundleName);
277 if (bundlesByLocale != null)
278 {
279 // Cache of bundles by locale for the named bundle exists.
280 // Check the cache for a bundle corresponding to locale.
281 rb = (ResourceBundle) bundlesByLocale.get(locale);
282 if (rb == null)
283 {
284 // Not yet cached.
285 rb = cacheBundle(bundleName, locale);
286 }
287 }
288 else
289 {
290 rb = cacheBundle(bundleName, locale);
291 }
292 return rb;
293 }
294
295 /**
296 * Caches the named bundle for fast lookups. This operation is
297 * relatively expensive in terms of memory use, but is optimized
298 * for run-time speed in the usual case.
299 *
300 * @exception MissingResourceException Bundle not found.
301 */
302 private synchronized ResourceBundle cacheBundle(
303 String bundleName,
304 Locale locale)
305 throws MissingResourceException
306 {
307 HashMap bundlesByLocale = (HashMap) bundles.get(bundleName);
308 ResourceBundle rb =
309 (bundlesByLocale == null
310 ? null
311 : (ResourceBundle) bundlesByLocale.get(locale));
312 if (rb == null)
313 {
314 bundlesByLocale =
315 (bundlesByLocale == null
316 ? new HashMap(3)
317 : new HashMap(bundlesByLocale));
318 try
319 {
320 rb = ResourceBundle.getBundle(bundleName, locale);
321 }
322 catch (MissingResourceException e)
323 {
324 rb = findBundleByLocale(bundleName, locale, bundlesByLocale);
325 if (rb == null)
326 {
327 throw (MissingResourceException) e.fillInStackTrace();
328 }
329 }
330 if (rb != null)
331 {
332 // Cache bundle.
333 bundlesByLocale.put(rb.getLocale(), rb);
334 HashMap bundlesByName = new HashMap(bundles);
335 bundlesByName.put(bundleName, bundlesByLocale);
336 this.bundles = bundlesByName;
337 }
338 }
339 return rb;
340 }
341
342 /**
343 * <p>Retrieves the bundle most closely matching first against the
344 * supplied inputs, then against the defaults.</p>
345 *
346 * <p>Use case: some clients send a HTTP Accept-Language header
347 * with a value of only the language to use
348 * (i.e. "Accept-Language: en"), and neglect to include a country.
349 * When there is no bundle for the requested language, this method
350 * can be called to try the default country (checking internally
351 * to assure the requested criteria matches the default to avoid
352 * disconnects between language and country).</p>
353 *
354 * <p>Since we're really just guessing at possible bundles to use,
355 * we don't ever throw <code>MissingResourceException</code>.</p>
356 */
357 private ResourceBundle findBundleByLocale(
358 String bundleName,
359 Locale locale,
360 Map bundlesByLocale)
361 {
362 ResourceBundle rb = null;
363 if (!StringUtils.isNotEmpty(locale.getCountry())
364 && defaultLanguage.equals(locale.getLanguage()))
365 {
366 /*
367 * category.debug("Requested language '" + locale.getLanguage() +
368 * "' matches default: Attempting to guess bundle " +
369 * "using default country '" + defaultCountry + '\'');
370 */
371 Locale withDefaultCountry =
372 new Locale(locale.getLanguage(), defaultCountry);
373 rb = (ResourceBundle) bundlesByLocale.get(withDefaultCountry);
374 if (rb == null)
375 {
376 rb = getBundleIgnoreException(bundleName, withDefaultCountry);
377 }
378 }
379 else if (
380 !StringUtils.isNotEmpty(locale.getLanguage())
381 && defaultCountry.equals(locale.getCountry()))
382 {
383 Locale withDefaultLanguage =
384 new Locale(defaultLanguage, locale.getCountry());
385 rb = (ResourceBundle) bundlesByLocale.get(withDefaultLanguage);
386 if (rb == null)
387 {
388 rb = getBundleIgnoreException(bundleName, withDefaultLanguage);
389 }
390 }
391 if (rb == null && !defaultLocale.equals(locale))
392 {
393 rb = getBundleIgnoreException(bundleName, defaultLocale);
394 }
395 return rb;
396 }
397
398 /**
399 * Retrieves the bundle using the
400 * <code>ResourceBundle.getBundle(String, Locale)</code> method,
401 * returning <code>null</code> instead of throwing
402 * <code>MissingResourceException</code>.
403 */
404 private final ResourceBundle getBundleIgnoreException(
405 String bundleName,
406 Locale locale)
407 {
408 try
409 {
410 return ResourceBundle.getBundle(bundleName, locale);
411 }
412 catch (MissingResourceException ignored)
413 {
414 return null;
415 }
416 }
417
418 /**
419 * This method sets the name of the first bundle in the search
420 * list (the "default" bundle).
421 *
422 * @param defaultBundle Name of default bundle.
423 */
424 public void setBundle(String defaultBundle)
425 {
426 if (bundleNames.length > 0)
427 {
428 bundleNames[0] = defaultBundle;
429 }
430 else
431 {
432 synchronized (this)
433 {
434 if (bundleNames.length <= 0)
435 {
436 bundleNames = new String[] { defaultBundle };
437 }
438 }
439 }
440 }
441
442 /**
443 * @exception MissingResourceException Specified key cannot be matched.
444 * @see org.apache.fulcrum.localization.SimpleLocalizationService#getString(String, Locale, String)
445 */
446 public String getString(String bundleName, Locale locale, String key)
447 throws MissingResourceException
448 {
449 String value = null;
450 if (locale == null)
451 {
452 locale = getDefaultLocale();
453 }
454 // Look for text in requested bundle.
455 ResourceBundle rb = getBundle(bundleName, locale);
456 value = getStringOrNull(rb, key);
457 // Look for text in list of default bundles.
458 if (value == null && bundleNames.length > 0)
459 {
460 String name;
461 for (int i = 0; i < bundleNames.length; i++)
462 {
463 name = bundleNames[i];
464 //System.out.println("getString(): name=" + name +
465 // ", locale=" + locale + ", i=" + i);
466 if (!name.equals(bundleName))
467 {
468 rb = getBundle(name, locale);
469 value = getStringOrNull(rb, key);
470 if (value != null)
471 {
472 locale = rb.getLocale();
473 break;
474 }
475 }
476 }
477 }
478 if (value == null)
479 {
480 String loc = locale.toString();
481 String mesg =
482 LocalizationService.SERVICE_NAME
483 + " noticed missing resource: "
484 + "bundleName="
485 + bundleName
486 + ", locale="
487 + loc
488 + ", key="
489 + key;
490 getLogger().debug(mesg);
491 // Text not found in requested or default bundles.
492 throw new MissingResourceException(mesg, bundleName, key);
493 }
494 return value;
495 }
496
497 /**
498 * Gets localized text from a bundle if it's there. Otherwise,
499 * returns <code>null</code> (ignoring a possible
500 * <code>MissingResourceException</code>).
501 */
502 protected final String getStringOrNull(ResourceBundle rb, String key)
503 {
504 if (rb != null)
505 {
506 try
507 {
508 return rb.getString(key);
509 }
510 catch (MissingResourceException ignored)
511 {
512 // ignore
513 }
514 }
515 return null;
516 }
517
518 /**
519 * @see org.apache.fulcrum.localization.SimpleLocalizationService#format(String, Locale, String, Object)
520 */
521 public String format(
522 String bundleName,
523 Locale locale,
524 String key,
525 Object arg1)
526 {
527 return format(bundleName, locale, key, new Object[] { arg1 });
528 }
529
530 /**
531 * @see org.apache.fulcrum.localization.SimpleLocalizationService#format(String, Locale, String, Object, Object)
532 */
533 public String format(
534 String bundleName,
535 Locale locale,
536 String key,
537 Object arg1,
538 Object arg2)
539 {
540 return format(bundleName, locale, key, new Object[] { arg1, arg2 });
541 }
542
543 /**
544 * Looks up the value for <code>key</code> in the
545 * <code>ResourceBundle</code> referenced by
546 * <code>bundleName</code>, then formats that value for the
547 * specified <code>Locale</code> using <code>args</code>.
548 *
549 * @return Localized, formatted text identified by
550 * <code>key</code>.
551 */
552 public String format(
553 String bundleName,
554 Locale locale,
555 String key,
556 Object[] args)
557 {
558 // When formatting Date objects and such, MessageFormat
559 // cannot have a null Locale.
560 Locale formatLocale = (locale == null) ? getDefaultLocale() : locale;
561 String value = getString(bundleName, locale, key);
562
563 Object[] formatArgs = (args == null) ? NO_ARGS : args;
564
565 MessageFormat messageFormat = new MessageFormat(value, formatLocale);
566 return messageFormat.format(formatArgs);
567 }
568 }