View Javadoc

1   /**
2    * Copyright (C) 2005-2009 Alfresco Software Limited.
3    *
4    * This file is part of the Spring Surf Extension project.
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  package org.springframework.extensions.surf.util;
20  
21  import java.text.MessageFormat;
22  import java.util.Enumeration;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.ResourceBundle;
29  import java.util.Set;
30  import java.util.StringTokenizer;
31  import java.util.concurrent.locks.Lock;
32  import java.util.concurrent.locks.ReadWriteLock;
33  import java.util.concurrent.locks.ReentrantReadWriteLock;
34  
35  /**
36   * Utility class providing methods to access the Locale of the current thread and to get
37   * Localised strings.
38   * 
39   * @author Roy Wetherall
40   */
41  public class I18NUtil
42  {
43      /**
44       * Thread-local containing the general Locale for the current thread
45       */
46      private static ThreadLocal<Locale> threadLocale = new ThreadLocal<Locale>();
47      
48      /**
49       * Thread-local containing the content Locale for for the current thread.  This
50       * can be used for content and property filtering.
51       */
52      private static ThreadLocal<Locale> threadContentLocale = new ThreadLocal<Locale>();
53      
54      /**
55       * List of registered bundles
56       */
57      private static Set<String> resouceBundleBaseNames = new HashSet<String>();
58      
59      /**
60       * Map of loaded bundles by Locale
61       */
62      private static Map<Locale, Set<String>> loadedResourceBundles = new HashMap<Locale, Set<String>>();
63      
64      /**
65       * Map of cached messaged by Locale
66       */
67      private static Map<Locale, Map<String, String>> cachedMessages = new HashMap<Locale, Map<String, String>>();
68      
69      /**
70       * Lock objects
71       */
72      private static ReadWriteLock lock = new ReentrantReadWriteLock();
73      private static Lock readLock = lock.readLock();
74      private static Lock writeLock = lock.writeLock();
75      
76      /**
77       * Set the locale for the current thread.
78       * 
79       * @param locale    the locale
80       */
81      public static void setLocale(Locale locale)
82      {
83          threadLocale.set(locale);
84      }
85  
86      /**
87       * Get the general local for the current thread, will revert to the default locale if none 
88       * specified for this thread.
89       * 
90       * @return  the general locale
91       */
92      public static Locale getLocale()
93      {
94          Locale locale = threadLocale.get(); 
95          if (locale == null)
96          {
97              // Get the default locale
98              locale = Locale.getDefault();
99          }
100         return locale;
101     }
102     
103     /**
104      * Set the <b>content locale</b> for the current thread.
105      * 
106      * @param locale    the content locale
107      */
108     public static void setContentLocale(Locale locale)
109     {
110         threadContentLocale.set(locale);
111     }
112 
113     /**
114      * Get the content local for the current thread.<br/>
115      * This will revert to {@link #getLocale()} if no value has been defined.
116      * 
117      * @return  Returns the content locale
118      */
119     public static Locale getContentLocale()
120     {
121         Locale locale = threadContentLocale.get(); 
122         if (locale == null)
123         {
124             // Revert to the normal locale
125             locale = getLocale();
126         }
127         return locale;
128     }
129     
130     /**
131 * Get the content local for the current thread.<br/>
132      * This will revert <tt>null</tt> if no value has been defined.
133      * 
134      * @return  Returns the content locale
135      */
136     public static Locale getContentLocaleOrNull()
137     {
138         Locale locale = threadContentLocale.get(); 
139         
140         return locale;
141     }
142     
143     
144     /**
145      * Searches for the nearest locale from the available options.  To match any locale, pass in
146      * <tt>null</tt>.
147      * 
148      * @param templateLocale the template to search for or <tt>null</tt> to match any locale
149      * @param options the available locales to search from
150      * @return Returns the best match from the available options, or the <tt>null</tt> if
151      *      all matches fail
152      */
153     public static Locale getNearestLocale(Locale templateLocale, Set<Locale> options)
154     {
155         if (options.isEmpty())                          // No point if there are no options
156         {
157             return null;
158         }
159         else if (templateLocale == null)
160         {
161             for (Locale locale : options)
162             {
163                 return locale;
164             }
165         }
166         else if (options.contains(templateLocale))      // First see if there is an exact match
167         {
168             return templateLocale;
169         }
170         // make a copy of the set
171         Set<Locale> remaining = new HashSet<Locale>(options);
172         
173         // eliminate those without matching languages
174         Locale lastMatchingOption = null;
175         String templateLanguage = templateLocale.getLanguage();
176         if (templateLanguage != null && !templateLanguage.equals(""))
177         {
178             Iterator<Locale> iterator = remaining.iterator();
179             while (iterator.hasNext())
180             {
181                 Locale option = iterator.next();
182                 if (option != null && !templateLanguage.equals(option.getLanguage()))
183                 {
184                     iterator.remove();                  // It doesn't match, so remove
185                 }
186                 else
187                 {
188                     lastMatchingOption = option;       // Keep a record of the last match
189                 }
190             }
191         }
192         if (remaining.isEmpty())
193         {
194             return null;
195         }
196         else if (remaining.size() == 1 && lastMatchingOption != null)
197         {
198             return lastMatchingOption;
199         }
200         
201         // eliminate those without matching country codes
202         lastMatchingOption = null;
203         String templateCountry = templateLocale.getCountry();
204         if (templateCountry != null && !templateCountry.equals(""))
205         {
206             Iterator<Locale> iterator = remaining.iterator();
207             while (iterator.hasNext())
208             {
209                 Locale option = iterator.next();
210                 if (option != null && !templateCountry.equals(option.getCountry()))
211                 {
212                     // It doesn't match language - remove
213                     // Don't remove the iterator. If it matchs a langage but not the country, returns any matched language                     
214                     // iterator.remove();
215                 }
216                 else
217                 {
218                     lastMatchingOption = option;       // Keep a record of the last match
219                 }
220             }
221         }
222         /*if (remaining.isEmpty())
223         {
224             return null;
225         }
226         else */
227         if (remaining.size() == 1 && lastMatchingOption != null)
228         {
229             return lastMatchingOption;
230         }
231         else
232         {
233             // We have done an earlier equality check, so there isn't a matching variant
234             // Also, we know that there are multiple options at this point, either of which will do.
235         	
236         	// This gets any country match (there will be worse matches so we take the last the country match)
237         	if(lastMatchingOption != null)
238         	{
239         		return lastMatchingOption;
240         	}
241         	else
242         	{
243                 for (Locale locale : remaining)
244                 {
245                     return locale;
246                 }
247         	}
248         }
249         // The logic guarantees that this code can't be called
250         throw new RuntimeException("Logic should not allow code to get here.");
251     }
252     
253     /**
254      * Factory method to create a Locale from a <tt>lang_country_variant</tt> string.
255      * 
256      * @param localeStr e.g. fr_FR
257      * @return Returns the locale instance, or the {@link Locale#getDefault() default} if the
258      *      string is invalid
259      */
260     public static Locale parseLocale(String localeStr)
261     {
262         if(localeStr == null)
263         {
264             return null; 
265         }
266         Locale locale = Locale.getDefault();
267         
268         StringTokenizer t = new StringTokenizer(localeStr, "_");
269         int tokens = t.countTokens();
270         if (tokens == 1)
271         {
272            locale = new Locale(t.nextToken());
273         }
274         else if (tokens == 2)
275         {
276            locale = new Locale(t.nextToken(), t.nextToken());
277         }
278         else if (tokens == 3)
279         {
280            locale = new Locale(t.nextToken(), t.nextToken(), t.nextToken());
281         }
282         
283         return locale;
284     }
285     
286     /**
287      * Register a resource bundle.
288      * <p>
289      * This should be the bundle base name eg, spring-surf.messages.errors
290      * <p>
291      * Once registered the messges will be available via getMessage
292      * 
293      * @param bundleBaseName    the bundle base name
294      */
295     public static void registerResourceBundle(String bundleBaseName)
296     {
297         try
298         {
299             writeLock.lock();
300             resouceBundleBaseNames.add(bundleBaseName);
301         }
302         finally
303         {
304             writeLock.unlock();
305         }
306     }
307     
308     /**
309      * Get message from registered resource bundle.
310      * 
311      * @param messageKey    message key
312      * @return              localised message string, null if not found
313      */
314     public static String getMessage(String messageKey)
315     {
316         return getMessage(messageKey, getLocale());
317     }
318     
319     /**
320      * Get a localised message string
321      * 
322      * @param messageKey        the message key
323      * @param locale            override the current locale
324      * @return                  the localised message string, null if not found
325      */
326     public static String getMessage(String messageKey, Locale locale)
327     {
328         String message = null;
329         Map<String, String> props = getLocaleProperties(locale);
330         if (props != null)
331         {
332             message = props.get(messageKey);
333         }
334         return message;
335     }
336     
337     /**
338      * Get a localised message string, parameterized using standard MessageFormatter.
339      * 
340      * @param messageKey    message key
341      * @param params        format parameters
342      * @return              the localised string, null if not found
343      */
344     public static String getMessage(String messageKey, Object ... params)
345     {
346         return getMessage(messageKey, getLocale(), params);
347     }
348     
349     /**
350      * Get a localised message string, parameterized using standard MessageFormatter.
351      * 
352      * @param messageKey        the message key
353      * @param locale            override current locale
354      * @param params            the localised message string
355      * @return                  the localaised string, null if not found
356      */
357     public static String getMessage(String messageKey, Locale locale, Object ... params)
358     {
359         String message = getMessage(messageKey, locale);
360         if (message != null && params != null)
361         {
362             message = MessageFormat.format(message, params);
363         }
364         return message;
365     }
366     
367     /**
368      * @return the map of all available messages for the current locale
369      */
370     public static Map<String, String> getAllMessages()
371     {
372         return getLocaleProperties(getLocale());
373     }
374     
375     /**
376      * @return the map of all available messages for the specified locale
377      */
378     public static Map<String, String> getAllMessages(Locale locale)
379     {
380         return getLocaleProperties(locale);
381     }
382     
383     /**
384      * Get the messages for a locale.
385      * <p>
386      * Will use cache where available otherwise will load into cache from bundles.
387      * 
388      * @param locale    the locale
389      * @return          message map
390      */
391     private static Map<String, String> getLocaleProperties(Locale locale)
392     {
393         Set<String> loadedBundles = null;
394         Map<String, String> props = null;
395         int loadedBundleCount = 0;
396         try
397         {
398             readLock.lock();
399             loadedBundles = loadedResourceBundles.get(locale);
400             props = cachedMessages.get(locale);
401             loadedBundleCount = resouceBundleBaseNames.size();
402         }
403         finally
404         {
405             readLock.unlock();
406         }
407         
408         if (loadedBundles == null)
409         {
410             try
411             {
412                 writeLock.lock();
413                 loadedBundles = new HashSet<String>();
414                 loadedResourceBundles.put(locale, loadedBundles);
415             }
416             finally
417             {
418                 writeLock.unlock();
419             }
420         }
421         
422         if (props == null)
423         {
424             try
425             {
426                 writeLock.lock();
427                 props = new HashMap<String, String>();
428                 cachedMessages.put(locale, props);
429             }
430             finally
431             {
432                 writeLock.unlock();
433             }
434         }
435                 
436         if (loadedBundles.size() != loadedBundleCount)
437         {
438             try
439             {
440                 writeLock.lock();
441                 for (String resourceBundleBaseName : resouceBundleBaseNames)
442                 {
443                     if (loadedBundles.contains(resourceBundleBaseName) == false)
444                     {
445                         ResourceBundle resourcebundle = ResourceBundle.getBundle(resourceBundleBaseName, locale);
446                         Enumeration<String> enumKeys = resourcebundle.getKeys();
447                         while (enumKeys.hasMoreElements() == true)
448                         {
449                             String key = enumKeys.nextElement();
450                             props.put(key, resourcebundle.getString(key));
451                         }
452                         loadedBundles.add(resourceBundleBaseName);
453                     }
454                 }
455             }
456             finally
457             {
458                 writeLock.unlock();
459             }
460         }
461         
462         return props;
463     }
464 }