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    }