blob: 4d26d556f0a4f1ed3e0e3c696db067e1bd06a425 [file] [log] [blame]
package org.apache.fulcrum.localization;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.commons.lang3.StringUtils;
/**
* <p>
* This class is the single point of access to all localization
* resources. It caches different ResourceBundles for different
* Locales.
* </p>
*
* <p>Usage example:</p>
*
* <pre>
* <code>
* SimpleLocalizationService ls = (SimpleLocalizationService) TurbineServices
* .getInstance().getService(SimpleLocalizationService.SERVICE_NAME);
* </code>
* </pre>
*
* <p>
* Then call {@link #getString(String, Locale, String)}, or one of
* two methods to retrieve a ResourceBundle:
* </p>
*
* <ul>
* <li>getBundle("MyBundleName")</li>
* <li>getBundle("MyBundleName", Locale)</li>
* <li>etc.</li>
* </ul>
*
* @author <a href="mailto:jm@mediaphil.de">Jonas Maurus</a>
* @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
* @author <a href="mailto:novalidemail@foo.com">Frank Y. Kim</a>
* @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
* @author <a href="mailto:leonardr@collab.net">Leonard Richardson</a>
* @author <a href="mailto:mcconnell@apache.org">Stephen McConnell</a>
* @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
* @version $Id: DefaultLocalizationService.java 535465 2007-05-05 06:58:06Z tv $
*
* avalon.component name="localization" lifestyle="singleton"
* avalon.service type="org.apache.fulcrum.localization.SimpleLocalizationService"
*/
public class SimpleLocalizationServiceImpl
extends AbstractLogEnabled
implements SimpleLocalizationService, Configurable, Initializable
{
/** Key Prefix for our bundles */
private static final String BUNDLES = "bundles";
/**
* The value to pass to <code>MessageFormat</code> if a
* <code>null</code> reference is passed to <code>format()</code>.
*/
private static final Object[] NO_ARGS = new Object[0];
/**
* Bundle name keys a HashMap of the ResourceBundles in this
* service (which is in turn keyed by Locale).
*/
private HashMap<String, HashMap<Locale, ResourceBundle>> bundles = null;
/**
* The list of default bundles to search.
*/
private String[] bundleNames = null;
/**
* The name of the default locale to use (includes language and
* country).
*/
private Locale defaultLocale = null;
/** The name of the default language to use. */
private String defaultLanguage = null;
/** The name of the default country to use. */
private String defaultCountry = null;
/**
* Creates a new instance.
*/
public SimpleLocalizationServiceImpl()
{
bundles = new HashMap<String, HashMap<Locale, ResourceBundle>>();
}
/**
* Avalon lifecycle method
*
* {@link org.apache.avalon.framework.configuration.Configurable}
*
* @param conf the configuration
* @throws ConfigurationException if failed to configure
*/
@Override
public void configure(Configuration conf) throws ConfigurationException
{
Locale jvmDefault = Locale.getDefault();
defaultLanguage =
conf
.getAttribute(
"locale-default-language",
jvmDefault.getLanguage())
.trim();
defaultCountry =
conf
.getAttribute("locale-default-country", jvmDefault.getCountry())
.trim();
// FIXME! need to add bundle names
getLogger().info(
"initialized lang="
+ defaultLanguage
+ " country="
+ defaultCountry);
final Configuration bundles = conf.getChild(BUNDLES, false);
if (bundles != null)
{
Configuration[] nameVal = bundles.getChildren();
String bundleName[] = new String[nameVal.length];
for (int i = 0; i < nameVal.length; i++)
{
String val = nameVal[i].getValue();
getLogger().debug("Registered bundle " + val);
bundleName[i] = val;
}
initBundleNames(bundleName);
}
}
/**
* Called the first time the Service is used.
*
* @throws Exception generic exception
*/
@Override
public void initialize() throws Exception
{
defaultLocale = new Locale(defaultLanguage, defaultCountry);
if (getLogger().isInfoEnabled())
{
getLogger().info("Localization Service is Initialized now..");
}
}
/**
* Initialize list of default bundle names.
*
* @param intBundleNames set bundle names
*/
protected void initBundleNames(String[] intBundleNames)
{
if (intBundleNames == null)
bundleNames = new String[0];
else
bundleNames = intBundleNames;
}
/**
* Retrieves the default language (specified in the config file).
*
* @return the default language
*/
@Override
public String getDefaultLanguage()
{
return defaultLanguage;
}
/**
* Retrieves the default country (specified in the config file).
*
* @return the default country
*/
@Override
public String getDefaultCountry()
{
return defaultCountry;
}
/**
* Retrieves the default Locale (as created from default
* language and default country).
*
* @return the default locale
*/
@Override
public Locale getDefaultLocale()
{
return defaultLocale;
}
/**
* {@link org.apache.fulcrum.localization.SimpleLocalizationService#getDefaultBundleName()}
*
* @return the default bundle name
*/
@Override
public String getDefaultBundleName()
{
return (bundleNames.length > 0 ? bundleNames[0] : "");
}
/**
* {@link org.apache.fulcrum.localization.SimpleLocalizationService#getBundleNames()}
*
* @return list of bundle names available
*/
@Override
public String[] getBundleNames()
{
return bundleNames.clone();
}
/**
* @see org.apache.fulcrum.localization.SimpleLocalizationService#getBundle()
*
* @return the default resource bundle
*/
@Override
public ResourceBundle getBundle()
{
return getBundle(getDefaultBundleName(), (Locale) null);
}
/**
* @see org.apache.fulcrum.localization.SimpleLocalizationService#getBundle(java.lang.String)
*
* @param bundleName the name of a bundle
* @return the resource bundle
*/
@Override
public ResourceBundle getBundle(String bundleName)
{
return getBundle(bundleName, (Locale) null);
}
/**
* @see org.apache.fulcrum.localization.SimpleLocalizationService#getBundle(java.lang.String, java.util.Locale)
*
* This method returns a ResourceBundle for the given bundle name
* and the given Locale.
*
* @param bundleName Name of bundle (or <code>null</code> for the
* default bundle).
* @param locale The locale (or <code>null</code> for the locale
* indicated by the default language and country).
* @return A localized ResourceBundle.
*/
@Override
public ResourceBundle getBundle(String bundleName, Locale locale)
{
// Assure usable inputs.
bundleName =
(bundleName == null ? getDefaultBundleName() : bundleName.trim());
if (locale == null)
{
locale = getDefaultLocale();
}
// Find/retrieve/cache bundle.
ResourceBundle rb = null;
HashMap<?, ?> bundlesByLocale = (HashMap<?, ?>) bundles.get(bundleName);
if (bundlesByLocale != null)
{
// Cache of bundles by locale for the named bundle exists.
// Check the cache for a bundle corresponding to locale.
rb = (ResourceBundle) bundlesByLocale.get(locale);
if (rb == null)
{
// Not yet cached.
rb = cacheBundle(bundleName, locale);
}
}
else
{
rb = cacheBundle(bundleName, locale);
}
return rb;
}
/**
* Caches the named bundle for fast lookups. This operation is
* relatively expensive in terms of memory use, but is optimized
* for run-time speed in the usual case.
*
* @param bundleName Name of bundle (or <code>null</code> for the
* default bundle).
* @param locale The locale (or <code>null</code> for the locale
* indicated by the default language and country).
* @throws MissingResourceException Bundle not found.
* @return a localized resource bundle
*/
private synchronized ResourceBundle cacheBundle(
String bundleName,
Locale locale)
throws MissingResourceException
{
HashMap<Locale, ResourceBundle> bundlesByLocale = (HashMap<Locale, ResourceBundle>) bundles.get(bundleName);
ResourceBundle rb =
(bundlesByLocale == null
? null
: (ResourceBundle) bundlesByLocale.get(locale));
if (rb == null)
{
bundlesByLocale =
(bundlesByLocale == null
? new HashMap<Locale, ResourceBundle>(3)
: new HashMap<Locale, ResourceBundle>(bundlesByLocale));
try
{
rb = ResourceBundle.getBundle(bundleName, locale);
}
catch (MissingResourceException e)
{
rb = findBundleByLocale(bundleName, locale, bundlesByLocale);
if (rb == null)
{
throw (MissingResourceException) e.fillInStackTrace();
}
}
if (rb != null)
{
// Cache bundle.
bundlesByLocale.put(rb.getLocale(), rb);
HashMap<String, HashMap<Locale, ResourceBundle>> bundlesByName
= new HashMap<String, HashMap<Locale, ResourceBundle>>(bundles);
bundlesByName.put(bundleName, bundlesByLocale);
this.bundles = bundlesByName;
}
}
return rb;
}
/**
* <p>Retrieves the bundle most closely matching first against the
* supplied inputs, then against the defaults.</p>
*
* <p>Use case: some clients send a HTTP Accept-Language header
* with a value of only the language to use
* (i.e. "Accept-Language: en"), and neglect to include a country.
* When there is no bundle for the requested language, this method
* can be called to try the default country (checking internally
* to assure the requested criteria matches the default to avoid
* disconnects between language and country).</p>
*
* <p>Since we're really just guessing at possible bundles to use,
* we don't ever throw <code>MissingResourceException</code>.</p>
*
* @param bundleName Name of bundle (or <code>null</code> for the
* default bundle).
* @param locale The locale (or <code>null</code> for the locale
* indicated by the default language and country).
* @param bundleByLocale map of locales and resource bundles
* @return a localized resource bundle
*
*/
private ResourceBundle findBundleByLocale(
String bundleName,
Locale locale,
Map<Locale, ResourceBundle> bundlesByLocale)
{
ResourceBundle rb = null;
if (StringUtils.isEmpty(locale.getCountry())
&& defaultLanguage.equals(locale.getLanguage()))
{
/*
* category.debug("Requested language '" + locale.getLanguage() +
* "' matches default: Attempting to guess bundle " +
* "using default country '" + defaultCountry + '\'');
*/
Locale withDefaultCountry =
new Locale(locale.getLanguage(), defaultCountry);
rb = (ResourceBundle) bundlesByLocale.get(withDefaultCountry);
if (rb == null)
{
rb = getBundleIgnoreException(bundleName, withDefaultCountry);
}
}
else if (
StringUtils.isEmpty(locale.getLanguage())
&& defaultCountry.equals(locale.getCountry()))
{
Locale withDefaultLanguage =
new Locale(defaultLanguage, locale.getCountry());
rb = (ResourceBundle) bundlesByLocale.get(withDefaultLanguage);
if (rb == null)
{
rb = getBundleIgnoreException(bundleName, withDefaultLanguage);
}
}
if (rb == null && !defaultLocale.equals(locale))
{
rb = getBundleIgnoreException(bundleName, defaultLocale);
}
return rb;
}
/**
* Retrieves the bundle using the
* <code>ResourceBundle.getBundle(String, Locale)</code> method,
* returning <code>null</code> instead of throwing
* <code>MissingResourceException</code>.
*
* @param bundleName Name of bundle (or <code>null</code> for the
* default bundle).
* @param locale The locale (or <code>null</code> for the locale
* indicated by the default language and country).
*/
private final ResourceBundle getBundleIgnoreException(
String bundleName,
Locale locale)
{
try
{
return ResourceBundle.getBundle(bundleName, locale);
}
catch (MissingResourceException ignored)
{
return null;
}
}
/**
* This method sets the name of the first bundle in the search
* list (the "default" bundle).
*
* @param defaultBundle Name of default bundle.
*/
@Override
public void setBundle(String defaultBundle)
{
if (bundleNames.length > 0)
{
bundleNames[0] = defaultBundle;
}
else
{
synchronized (this)
{
if (bundleNames.length <= 0)
{
bundleNames = new String[] { defaultBundle };
}
}
}
}
/**
*
* {@link org.apache.fulcrum.localization.SimpleLocalizationService#getString(String, Locale, String)}
* @throws MissingResourceException Specified key cannot be matched.
*/
@Override
public String getString(String bundleName, Locale locale, String key)
throws MissingResourceException
{
String value = null;
if (locale == null)
{
locale = getDefaultLocale();
}
// Look for text in requested bundle.
ResourceBundle rb = getBundle(bundleName, locale);
value = getStringOrNull(rb, key);
// Look for text in list of default bundles.
if (value == null && bundleNames.length > 0)
{
String name;
for (int i = 0; i < bundleNames.length; i++)
{
name = bundleNames[i];
//System.out.println("getString(): name=" + name +
// ", locale=" + locale + ", i=" + i);
if (!name.equals(bundleName))
{
rb = getBundle(name, locale);
value = getStringOrNull(rb, key);
if (value != null)
{
locale = rb.getLocale();
break;
}
}
}
}
if (value == null)
{
String loc = locale.toString();
String mesg =
LocalizationService.SERVICE_NAME
+ " noticed missing resource: "
+ "bundleName="
+ bundleName
+ ", locale="
+ loc
+ ", key="
+ key;
getLogger().debug(mesg);
// Text not found in requested or default bundles.
throw new MissingResourceException(mesg, bundleName, key);
}
return value;
}
/**
* Returns the value for the key in the default bundle and the default locale.
*
* @param key The key to retrieve the value for.
* @return The value mapped to the key.
*/
@Override
public String getString(String key)
{
return getString(getDefaultBundleName(), getDefaultLocale(), key);
}
/**
* Gets localized text from a bundle if it's there. Otherwise,
* returns <code>null</code> (ignoring a possible
* <code>MissingResourceException</code>).
*
* @param rb resource bundle
* @param key The key to retrieve the value for.
* @return name of resource
*/
protected final String getStringOrNull(ResourceBundle rb, String key)
{
if (rb != null)
{
try
{
return rb.getString(key);
}
catch (MissingResourceException ignored)
{
// ignore
}
}
return null;
}
/**
* {@link org.apache.fulcrum.localization.SimpleLocalizationService#format(String, Locale, String, Object)}
* @param bundleName the bundle name
* @param locale locale
* @param key key to lookup
* @param arg1 bundle arguments
*/
@Override
public String format(
String bundleName,
Locale locale,
String key,
Object arg1)
{
return format(bundleName, locale, key, new Object[] { arg1 });
}
/* (non-Javadoc)
* @see org.apache.fulcrum.localization.SimpleLocalizationService#format(java.lang.String, java.util.Locale, java.lang.String, java.lang.Object, java.lang.Object)
*/
@Override
public String format(
String bundleName,
Locale locale,
String key,
Object arg1,
Object arg2)
{
return format(bundleName, locale, key, new Object[] { arg1, arg2 });
}
/**
* Looks up the value for <code>key</code> in the
* <code>ResourceBundle</code> referenced by
* <code>bundleName</code>, then formats that value for the
* specified <code>Locale</code> using <code>args</code>.
*
* If <code>locale</code> is <code>null</code>, {@link #getDefaultLocale()} will be checked.
* If <code>bundleName</code> is <code>null</code>, {@link #getDefaultBundleName()} will be checked (cft. {@link #getBundle(String, Locale)}.
*
* @return Localized, formatted text identified by
* <code>key</code>.
*/
@Override
public String format(
String bundleName,
Locale locale,
String key,
Object[] args)
{
// When formatting Date objects and such, MessageFormat
// cannot have a null Locale.
Locale formatLocale = (locale == null) ? getDefaultLocale() : locale;
String value = getString(bundleName, locale, key);
Object[] formatArgs = (args == null) ? NO_ARGS : args;
MessageFormat messageFormat = new MessageFormat(value, formatLocale);
return messageFormat.format(formatArgs);
}
}