View Javadoc
1   package org.codehaus.plexus.i18n;
2   
3   /*
4    * Copyright 2001-2007 Codehaus Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import org.codehaus.plexus.logging.AbstractLogEnabled;
20  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
21  import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
22  import org.codehaus.plexus.util.StringUtils;
23  
24  import java.text.MessageFormat;
25  import java.util.HashMap;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.MissingResourceException;
29  import java.util.ResourceBundle;
30  import java.lang.reflect.Field;
31  
32  /**
33   * @plexus.component
34   *   role="org.codehaus.plexus.i18n.I18N"
35   */
36  public class DefaultI18N
37      extends AbstractLogEnabled
38      implements I18N, Initializable
39  {
40      private static final Object[] NO_ARGS = new Object[0];
41  
42      private HashMap bundles;
43  
44      private String[] bundleNames;
45  
46      private String defaultBundleName;
47  
48      private Locale defaultLocale = Locale.getDefault();
49  
50      private String defaultLanguage = Locale.getDefault().getLanguage();
51  
52      private String defaultCountry = Locale.getDefault().getCountry();
53  
54      private boolean devMode;
55  
56      // ----------------------------------------------------------------------
57      // Accessors
58      // ----------------------------------------------------------------------
59  
60      public String getDefaultLanguage()
61      {
62          return defaultLanguage;
63      }
64  
65      public String getDefaultCountry()
66      {
67          return defaultCountry;
68      }
69  
70      public String getDefaultBundleName()
71      {
72          return defaultBundleName;
73      }
74  
75      public String[] getBundleNames()
76      {
77          return (String[]) bundleNames.clone();
78      }
79  
80      public ResourceBundle getBundle()
81      {
82          return getBundle( getDefaultBundleName(), (Locale) null );
83      }
84  
85      public ResourceBundle getBundle( String bundleName )
86      {
87          return getBundle( bundleName, (Locale) null );
88      }
89  
90      /**
91       * This method returns a ResourceBundle given the bundle name and
92       * the Locale information supplied in the HTTP "Accept-Language"
93       * header.
94       *
95       * @param bundleName     Name of bundle.
96       * @param languageHeader A String with the language header.
97       * @return A localized ResourceBundle.
98       */
99      public ResourceBundle getBundle( String bundleName, String languageHeader )
100     {
101         return getBundle( bundleName, getLocale( languageHeader ) );
102     }
103 
104     /**
105      * This method returns a ResourceBundle for the given bundle name
106      * and the given Locale.
107      *
108      * @param bundleName Name of bundle (or <code>null</code> for the
109      *                   default bundle).
110      * @param locale     The locale (or <code>null</code> for the locale
111      *                   indicated by the default language and country).
112      * @return A localized ResourceBundle.
113      */
114     public ResourceBundle getBundle( String bundleName, Locale locale )
115     {
116         // Assure usable inputs.
117         bundleName = ( bundleName == null ? getDefaultBundleName() : bundleName.trim() );
118 
119         // ----------------------------------------------------------------------
120         // A hack to make sure the properties files are always checked
121         // ----------------------------------------------------------------------
122 
123         if ( devMode )
124         {
125             try
126             {
127                 Class klass = ResourceBundle.getBundle( bundleName ).getClass().getSuperclass();
128 
129                 Field field = klass.getDeclaredField( "cacheList" );
130 
131                 field.setAccessible( true );
132 
133 //                SoftCache cache = (SoftCache) field.get( null );
134 //
135 //                cache.clear();
136 
137                 Object cache = field.get( null );
138 
139                 cache.getClass().getDeclaredMethod( "clear", null ).invoke( cache, null );
140 
141                 field.setAccessible( false );
142             }
143             catch ( Exception e )
144             {
145                 // Intentional
146             }
147         }
148 
149         if ( locale == null )
150         {
151             locale = getLocale( null );
152         }
153 
154         // Find/retrieve/cache bundle.
155         ResourceBundle rb;
156 
157         HashMap bundlesByLocale = (HashMap) bundles.get( bundleName );
158 
159         if ( bundlesByLocale != null )
160         {
161             // Cache of bundles by locale for the named bundle exists.
162             // Check the cache for a bundle corresponding to locale.
163             rb = (ResourceBundle) bundlesByLocale.get( locale );
164 
165             if ( rb == null )
166             {
167                 // Not yet cached.
168                 rb = cacheBundle( bundleName, locale );
169             }
170         }
171         else
172         {
173             rb = cacheBundle( bundleName, locale );
174         }
175 
176         return rb;
177     }
178 
179     /**
180      * @see I18N#getLocale(String)
181      */
182     public Locale getLocale( String header )
183     {
184         if ( !StringUtils.isEmpty( header ) )
185         {
186             I18NTokenizer tok = new I18NTokenizer( header );
187 
188             if ( tok.hasNext() )
189             {
190                 return (Locale) tok.next();
191             }
192         }
193 
194         // Couldn't parse locale.
195         return defaultLocale;
196     }
197 
198     public String getString( String key )
199     {
200         return getString( key, null );
201     }
202 
203     public String getString( String key, Locale locale )
204     {
205         return getString( getDefaultBundleName(), locale, key );
206     }
207 
208     /**
209      * @throws MissingResourceException Specified key cannot be matched.
210      * @see I18N#getString(String, Locale, String)
211      */
212     public String getString( String bundleName, Locale locale, String key )
213     {
214         String value;
215 
216         if ( locale == null )
217         {
218             locale = getLocale( null );
219         }
220 
221         // Look for text in requested bundle.
222         ResourceBundle rb = getBundle( bundleName, locale );
223 
224         value = getStringOrNull( rb, key );
225 
226         // Look for text in list of default bundles.
227         if ( value == null && bundleNames.length > 0 )
228         {
229             String name;
230             for ( int i = 0; i < bundleNames.length; i++ )
231             {
232                 name = bundleNames[i];
233 
234                 if ( !name.equals( bundleName ) )
235                 {
236                     rb = getBundle( name, locale );
237 
238                     value = getStringOrNull( rb, key );
239 
240                     if ( value != null )
241                     {
242                         locale = rb.getLocale();
243 
244                         break;
245                     }
246                 }
247             }
248         }
249 
250         if ( value == null )
251         {
252             String loc = locale.toString();
253 
254             String mesg = "Noticed missing resource: " + "bundleName=" + bundleName + ", locale=" + loc + ", key=" + key;
255 
256             getLogger().debug( mesg );
257 
258             // Just send back the key, we don't need to throw an exception.
259 
260             value = key;
261         }
262 
263         return value;
264     }
265 
266     public String format( String key, Object arg1 )
267     {
268         return format( defaultBundleName, defaultLocale, key, new Object[]{arg1} );
269     }
270 
271     public String format( String key, Object arg1, Object arg2 )
272     {
273         return format( defaultBundleName, defaultLocale, key, new Object[]{arg1, arg2} );
274     }
275 
276     /**
277      * @see I18N#format(String, Locale, String, Object)
278      */
279     public String format( String bundleName,
280                           Locale locale,
281                           String key,
282                           Object arg1 )
283     {
284         return format( bundleName, locale, key, new Object[]{arg1} );
285     }
286 
287     /**
288      * @see I18N#format(String, Locale, String, Object, Object)
289      */
290     public String format( String bundleName,
291                           Locale locale,
292                           String key,
293                           Object arg1,
294                           Object arg2 )
295     {
296         return format( bundleName, locale, key, new Object[]{arg1, arg2} );
297     }
298 
299     /**
300      * Looks up the value for <code>key</code> in the
301      * <code>ResourceBundle</code> referenced by
302      * <code>bundleName</code>, then formats that value for the
303      * specified <code>Locale</code> using <code>args</code>.
304      *
305      * @return Localized, formatted text identified by
306      *         <code>key</code>.
307      */
308     public String format( String bundleName, Locale locale, String key, Object[] args )
309     {
310         if ( locale == null )
311         {
312             // When formatting Date objects and such, MessageFormat
313             // cannot have a null Locale.
314             locale = getLocale( null );
315         }
316 
317         String value = getString( bundleName, locale, key );
318         if ( args == null )
319         {
320             args = NO_ARGS;
321         }
322 
323         // FIXME: after switching to JDK 1.4, it will be possible to clean
324         // this up by providing the Locale along with the string in the
325         // constructor to MessageFormat.  Until 1.4, the following workaround
326         // is required for constructing the format with the appropriate locale:
327         MessageFormat messageFormat = new MessageFormat( "" );
328         messageFormat.setLocale( locale );
329         messageFormat.applyPattern( value );
330 
331         return messageFormat.format( args );
332     }
333 
334 
335     /**
336      * Called the first time the Service is used.
337      */
338     public void initialize()
339         throws InitializationException
340     {
341         bundles = new HashMap();
342 
343         defaultLocale = new Locale( defaultLanguage, defaultCountry );
344 
345         initializeBundleNames();
346 
347         if ( "true".equals( System.getProperty( "PLEXUS_DEV_MODE" ) ) )
348         {
349             devMode = true;
350         }
351     }
352 
353     // ----------------------------------------------------------------------
354     // Implementation
355     // ----------------------------------------------------------------------
356 
357     protected void initializeBundleNames()
358     {
359         //System.err.println("cfg=" + getConfiguration());
360         if ( defaultBundleName != null && defaultBundleName.length() > 0 )
361         {
362             // Using old-style single bundle name property.
363             if ( bundleNames == null || bundleNames.length <= 0 )
364             {
365                 bundleNames = new String[]{defaultBundleName};
366             }
367             else
368             {
369                 // Prepend "default" bundle name.
370                 String[] array = new String[bundleNames.length + 1];
371                 array[0] = defaultBundleName;
372                 System.arraycopy( bundleNames, 0, array, 1, bundleNames.length );
373                 bundleNames = array;
374             }
375         }
376         if ( bundleNames == null )
377         {
378             bundleNames = new String[0];
379         }
380     }
381 
382     /**
383      * Caches the named bundle for fast lookups.  This operation is
384      * relatively expesive in terms of memory use, but is optimized
385      * for run-time speed in the usual case.
386      *
387      * @throws MissingResourceException Bundle not found.
388      */
389     private synchronized ResourceBundle cacheBundle( String bundleName, Locale locale )
390         throws MissingResourceException
391     {
392         HashMap bundlesByLocale = (HashMap) bundles.get( bundleName );
393 
394         ResourceBundle rb = ( bundlesByLocale == null ? null : (ResourceBundle) bundlesByLocale.get( locale ) );
395 
396         if ( rb == null )
397         {
398             bundlesByLocale = ( bundlesByLocale == null ? new HashMap( 3 ) : new HashMap( bundlesByLocale ) );
399 
400             try
401             {
402                 rb = ResourceBundle.getBundle( bundleName, locale );
403             }
404             catch ( MissingResourceException e )
405             {
406                 rb = findBundleByLocale( bundleName, locale, bundlesByLocale );
407 
408                 if ( rb == null )
409                 {
410                     throw (MissingResourceException) e.fillInStackTrace();
411                 }
412             }
413 
414             if ( rb != null )
415             {
416                 // Cache bundle.
417                 bundlesByLocale.put( rb.getLocale(), rb );
418 
419                 HashMap bundlesByName = new HashMap( bundles );
420 
421                 bundlesByName.put( bundleName, bundlesByLocale );
422 
423                 this.bundles = bundlesByName;
424             }
425         }
426 
427         return rb;
428     }
429 
430     /**
431      * <p>Retrieves the bundle most closely matching first against the
432      * supplied inputs, then against the defaults.</p>
433      * <p/>
434      * <p>Use case: some clients send a HTTP Accept-Language header
435      * with a value of only the language to use
436      * (i.e. "Accept-Language: en"), and neglect to include a country.
437      * When there is no bundle for the requested language, this method
438      * can be called to try the default country (checking internally
439      * to assure the requested criteria matches the default to avoid
440      * disconnects between language and country).</p>
441      * <p/>
442      * <p>Since we're really just guessing at possible bundles to use,
443      * we don't ever throw <code>MissingResourceException</code>.</p>
444      */
445     private ResourceBundle findBundleByLocale( String bundleName,
446                                                Locale locale,
447                                                Map bundlesByLocale )
448     {
449         ResourceBundle rb = null;
450 
451         if ( !StringUtils.isNotEmpty( locale.getCountry() ) &&
452              defaultLanguage.equals( locale.getLanguage() ) )
453         {
454             /*
455             category.debug("Requested language '" + locale.getLanguage() +
456                            "' matches default: Attempting to guess bundle " +
457                            "using default country '" + defaultCountry + '\'');
458             */
459             Locale withDefaultCountry = new Locale( locale.getLanguage(),
460                                                     defaultCountry );
461             rb = (ResourceBundle) bundlesByLocale.get( withDefaultCountry );
462             if ( rb == null )
463             {
464                 rb = getBundleIgnoreException( bundleName, withDefaultCountry );
465             }
466         }
467         else if ( !StringUtils.isNotEmpty( locale.getLanguage() ) &&
468                   defaultCountry.equals( locale.getCountry() ) )
469         {
470             Locale withDefaultLanguage = new Locale( defaultLanguage,
471                                                      locale.getCountry() );
472             rb = (ResourceBundle) bundlesByLocale.get( withDefaultLanguage );
473             if ( rb == null )
474             {
475                 rb = getBundleIgnoreException( bundleName, withDefaultLanguage );
476             }
477         }
478 
479         if ( rb == null && !defaultLocale.equals( locale ) )
480         {
481             rb = getBundleIgnoreException( bundleName, defaultLocale );
482         }
483 
484         return rb;
485     }
486 
487     /**
488      * Retrieves the bundle using the
489      * <code>ResourceBundle.getBundle(String, Locale)</code> method,
490      * returning <code>null</code> instead of throwing
491      * <code>MissingResourceException</code>.
492      */
493     private ResourceBundle getBundleIgnoreException( String bundleName, Locale locale )
494     {
495         try
496         {
497             return ResourceBundle.getBundle( bundleName, locale );
498         }
499         catch ( MissingResourceException ignored )
500         {
501             return null;
502         }
503     }
504 
505 
506     /**
507      * Gets localized text from a bundle if it's there.  Otherwise,
508      * returns <code>null</code> (ignoring a possible
509      * <code>MissingResourceException</code>).
510      */
511     protected final String getStringOrNull( ResourceBundle rb, String key )
512     {
513         if ( rb != null )
514         {
515             try
516             {
517                 return rb.getString( key );
518             }
519             catch ( MissingResourceException ignored )
520             {
521                 // intentional
522             }
523         }
524         return null;
525     }
526 
527 }