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 }