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 javax.inject.Named;
20  import javax.inject.Singleton;
21  
22  import java.text.MessageFormat;
23  import java.util.HashMap;
24  import java.util.Locale;
25  import java.util.Map;
26  import java.util.MissingResourceException;
27  import java.util.ResourceBundle;
28  
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  /**
33   *
34   */
35  @Named
36  @Singleton
37  public class DefaultI18N implements I18N {
38  
39      private final Logger log = LoggerFactory.getLogger(DefaultI18N.class);
40      private static final Object[] NO_ARGS = new Object[0];
41  
42      private Map<String, Map<Locale, ResourceBundle>> bundles;
43  
44      private String[] bundleNames;
45  
46      private String defaultBundleName;
47  
48      private boolean devMode;
49  
50      public DefaultI18N() {
51          initialize();
52      }
53  
54      public DefaultI18N(String[] bundleNames) {
55          this.bundleNames = bundleNames != null ? bundleNames.clone() : new String[0];
56          initialize();
57      }
58      // ----------------------------------------------------------------------
59      // Accessors
60      // ----------------------------------------------------------------------
61  
62      public String getDefaultLanguage() {
63          return Locale.getDefault().getLanguage();
64      }
65  
66      public String getDefaultCountry() {
67          return Locale.getDefault().getCountry();
68      }
69  
70      public String getDefaultBundleName() {
71          return defaultBundleName;
72      }
73  
74      public String[] getBundleNames() {
75          return bundleNames.clone();
76      }
77  
78      public ResourceBundle getBundle() {
79          return getBundle(getDefaultBundleName(), (Locale) null);
80      }
81  
82      public ResourceBundle getBundle(String bundleName) {
83          return getBundle(bundleName, (Locale) null);
84      }
85  
86      /**
87       * This method returns a ResourceBundle given the bundle name and
88       * the Locale information supplied in the HTTP "Accept-Language"
89       * header.
90       *
91       * @param bundleName     Name of bundle.
92       * @param languageHeader A String with the language header.
93       * @return A localized ResourceBundle.
94       */
95      public ResourceBundle getBundle(String bundleName, String languageHeader) {
96          return getBundle(bundleName, getLocale(languageHeader));
97      }
98  
99      /**
100      * This method returns a ResourceBundle for the given bundle name
101      * and the given Locale.
102      *
103      * @param bundleName Name of bundle (or <code>null</code> for the
104      *                   default bundle).
105      * @param locale     The locale (or <code>null</code> for the locale
106      *                   indicated by the default language and country).
107      * @return A localized ResourceBundle.
108      */
109     public ResourceBundle getBundle(String bundleName, Locale locale) {
110         // Assure usable inputs.
111         bundleName = (bundleName == null ? getDefaultBundleName() : bundleName.trim());
112 
113         // ----------------------------------------------------------------------
114         // A hack to make sure the properties files are always checked
115         // ----------------------------------------------------------------------
116 
117         if (devMode) {
118             ResourceBundle.clearCache();
119         }
120 
121         if (locale == null) {
122             locale = getLocale(null);
123         }
124 
125         // Find/retrieve/cache bundle.
126         ResourceBundle rb;
127 
128         Map<Locale, ResourceBundle> bundlesByLocale = bundles.get(bundleName);
129 
130         if (bundlesByLocale != null) {
131             // Cache of bundles by locale for the named bundle exists.
132             // Check the cache for a bundle corresponding to locale.
133             rb = bundlesByLocale.get(locale);
134             if (rb == null) {
135                 // Not yet cached.
136                 rb = cacheBundle(bundleName, locale);
137             }
138         } else {
139             rb = cacheBundle(bundleName, locale);
140         }
141 
142         return rb;
143     }
144 
145     /**
146      * @see I18N#getLocale(String)
147      */
148     public Locale getLocale(String header) {
149         if (header != null && !header.isEmpty()) {
150             I18NTokenizer tok = new I18NTokenizer(header);
151             if (tok.hasNext()) {
152                 return tok.next();
153             }
154         }
155 
156         // Couldn't parse locale.
157         return Locale.getDefault();
158     }
159 
160     public String getString(String key) {
161         return getString(key, null);
162     }
163 
164     public String getString(String key, Locale locale) {
165         return getString(getDefaultBundleName(), locale, key);
166     }
167 
168     /**
169      * @throws MissingResourceException Specified key cannot be matched.
170      * @see I18N#getString(String, Locale, String)
171      */
172     public String getString(String bundleName, Locale locale, String key) {
173         String value;
174         if (locale == null) {
175             locale = getLocale(null);
176         }
177 
178         // Look for text in requested bundle.
179         ResourceBundle rb = getBundle(bundleName, locale);
180 
181         value = getStringOrNull(rb, key);
182 
183         // Look for text in list of default bundles.
184         if (value == null) {
185             for (String name : bundleNames) {
186                 if (!name.equals(bundleName)) {
187                     rb = getBundle(name, locale);
188 
189                     value = getStringOrNull(rb, key);
190 
191                     if (value != null) {
192                         locale = rb.getLocale();
193 
194                         break;
195                     }
196                 }
197             }
198         }
199 
200         if (value == null) {
201             log.debug("Noticed missing resource: bundleName={}, locale={}, key={}", bundleName, locale, key);
202             // Just send back the key, we don't need to throw an exception.
203             value = key;
204         }
205 
206         return value;
207     }
208 
209     public String format(String key, Object arg1) {
210         return format(defaultBundleName, Locale.getDefault(), key, new Object[] {arg1});
211     }
212 
213     public String format(String key, Object arg1, Object arg2) {
214         return format(defaultBundleName, Locale.getDefault(), key, new Object[] {arg1, arg2});
215     }
216 
217     /**
218      * @see I18N#format(String, Locale, String, Object)
219      */
220     public String format(String bundleName, Locale locale, String key, Object arg1) {
221         return format(bundleName, locale, key, new Object[] {arg1});
222     }
223 
224     /**
225      * @see I18N#format(String, Locale, String, Object, Object)
226      */
227     public String format(String bundleName, Locale locale, String key, Object arg1, Object arg2) {
228         return format(bundleName, locale, key, new Object[] {arg1, arg2});
229     }
230 
231     /**
232      * Looks up the value for <code>key</code> in the
233      * <code>ResourceBundle</code> referenced by
234      * <code>bundleName</code>, then formats that value for the
235      * specified <code>Locale</code> using <code>args</code>.
236      *
237      * @return Localized, formatted text identified by
238      *         <code>key</code>.
239      */
240     public String format(String bundleName, Locale locale, String key, Object[] args) {
241         if (locale == null) {
242             // When formatting Date objects and such, MessageFormat
243             // cannot have a null Locale.
244             locale = getLocale(null);
245         }
246 
247         String value = getString(bundleName, locale, key);
248         if (args == null) {
249             args = NO_ARGS;
250         }
251         return new MessageFormat(value, locale).format(args);
252     }
253 
254     /**
255      * Called the first time the Service is used.
256      */
257     public void initialize() {
258         bundles = new HashMap<>();
259         initializeBundleNames();
260         if ("true".equals(System.getProperty("PLEXUS_DEV_MODE"))) {
261             devMode = true;
262         }
263     }
264 
265     // ----------------------------------------------------------------------
266     // Implementation
267     // ----------------------------------------------------------------------
268 
269     protected void initializeBundleNames() {
270         if (defaultBundleName != null && !defaultBundleName.isEmpty()) {
271             // Using old-style single bundle name property.
272             if (bundleNames == null || bundleNames.length <= 0) {
273                 bundleNames = new String[] {defaultBundleName};
274             } else {
275                 // Prepend "default" bundle name.
276                 String[] array = new String[bundleNames.length + 1];
277                 array[0] = defaultBundleName;
278                 System.arraycopy(bundleNames, 0, array, 1, bundleNames.length);
279                 bundleNames = array;
280             }
281         }
282         if (bundleNames == null) {
283             bundleNames = new String[0];
284         }
285     }
286 
287     /**
288      * Caches the named bundle for fast lookups.  This operation is
289      * relatively expesive in terms of memory use, but is optimized
290      * for run-time speed in the usual case.
291      *
292      * @throws MissingResourceException Bundle not found.
293      */
294     private synchronized ResourceBundle cacheBundle(String bundleName, Locale locale) throws MissingResourceException {
295         Map<Locale, ResourceBundle> bundlesByLocale = bundles.get(bundleName);
296 
297         ResourceBundle rb = (bundlesByLocale == null ? null : bundlesByLocale.get(locale));
298         if (rb == null) {
299             bundlesByLocale = (bundlesByLocale == null ? new HashMap<>(3) : new HashMap<>(bundlesByLocale));
300             try {
301                 rb = ResourceBundle.getBundle(
302                         bundleName,
303                         locale,
304                         ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT));
305             } catch (MissingResourceException e) {
306                 rb = findBundleByLocale(bundleName, locale, bundlesByLocale);
307                 if (rb == null) {
308                     throw (MissingResourceException) e.fillInStackTrace();
309                 }
310             }
311 
312             if (rb != null) {
313                 // Cache bundle.
314                 bundlesByLocale.put(rb.getLocale(), rb);
315                 Map<String, Map<Locale, ResourceBundle>> bundlesByName = new HashMap<>(bundles);
316                 bundlesByName.put(bundleName, bundlesByLocale);
317                 this.bundles = bundlesByName;
318             }
319         }
320 
321         return rb;
322     }
323 
324     /**
325      * <p>Retrieves the bundle most closely matching first against the
326      * supplied inputs, then against the defaults.</p>
327      * <p/>
328      * <p>Use case: some clients send a HTTP Accept-Language header
329      * with a value of only the language to use
330      * (i.e. "Accept-Language: en"), and neglect to include a country.
331      * When there is no bundle for the requested language, this method
332      * can be called to try the default country (checking internally
333      * to assure the requested criteria matches the default to avoid
334      * disconnects between language and country).</p>
335      * <p/>
336      * <p>Since we're really just guessing at possible bundles to use,
337      * we don't ever throw <code>MissingResourceException</code>.</p>
338      */
339     private ResourceBundle findBundleByLocale(
340             String bundleName, Locale locale, Map<Locale, ResourceBundle> bundlesByLocale) {
341         ResourceBundle rb = null;
342 
343         if (locale.getCountry() != null
344                 && !locale.getCountry().isEmpty()
345                 && Locale.getDefault().getLanguage().equals(locale.getLanguage())) {
346             Locale withDefaultCountry =
347                     new Locale(locale.getLanguage(), Locale.getDefault().getCountry());
348             rb = bundlesByLocale.get(withDefaultCountry);
349             if (rb == null) {
350                 rb = getBundleIgnoreException(bundleName, withDefaultCountry);
351             }
352         } else if (locale.getLanguage() != null
353                 && !locale.getLanguage().isEmpty()
354                 && Locale.getDefault().getCountry().equals(locale.getCountry())) {
355             Locale withDefaultLanguage = new Locale(Locale.getDefault().getLanguage(), locale.getCountry());
356             rb = bundlesByLocale.get(withDefaultLanguage);
357             if (rb == null) {
358                 rb = getBundleIgnoreException(bundleName, withDefaultLanguage);
359             }
360         }
361 
362         if (rb == null && !Locale.getDefault().equals(locale)) {
363             rb = getBundleIgnoreException(bundleName, Locale.getDefault());
364         }
365 
366         return rb;
367     }
368 
369     /**
370      * Retrieves the bundle using the
371      * <code>ResourceBundle.getBundle(String, Locale)</code> method,
372      * returning <code>null</code> instead of throwing
373      * <code>MissingResourceException</code>.
374      */
375     private ResourceBundle getBundleIgnoreException(String bundleName, Locale locale) {
376         try {
377             return ResourceBundle.getBundle(
378                     bundleName,
379                     locale,
380                     ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT));
381         } catch (MissingResourceException ignored) {
382             return null;
383         }
384     }
385 
386     /**
387      * Gets localized text from a bundle if it's there.  Otherwise,
388      * returns <code>null</code> (ignoring a possible
389      * <code>MissingResourceException</code>).
390      */
391     protected final String getStringOrNull(ResourceBundle rb, String key) {
392         if (rb != null) {
393             try {
394                 return rb.getString(key);
395             } catch (MissingResourceException ignored) {
396                 // intentional
397             }
398         }
399         return null;
400     }
401 }