View Javadoc
1   /*
2    * Copyright (c) 2002-2014, Mairie de Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.service.i18n;
35  
36  import fr.paris.lutece.portal.service.util.AppException;
37  import fr.paris.lutece.portal.service.util.AppLogService;
38  import fr.paris.lutece.portal.service.util.AppPathService;
39  import fr.paris.lutece.portal.service.util.AppPropertiesService;
40  import fr.paris.lutece.util.ReferenceList;
41  
42  import java.io.File;
43  
44  import java.net.MalformedURLException;
45  import java.net.URL;
46  import java.net.URLClassLoader;
47  
48  import java.text.DateFormat;
49  import java.text.MessageFormat;
50  
51  import java.util.ArrayList;
52  import java.util.Collection;
53  import java.util.Collections;
54  import java.util.Date;
55  import java.util.HashMap;
56  import java.util.List;
57  import java.util.Locale;
58  import java.util.Map;
59  import java.util.MissingResourceException;
60  import java.util.ResourceBundle;
61  import java.util.StringTokenizer;
62  import java.util.regex.Matcher;
63  import java.util.regex.Pattern;
64  
65  
66  /**
67   * This class provides services for internationalization (i18n)
68   * @since v1.4.1
69   */
70  public final class I18nService
71  {
72      private static final String FORMAT_PACKAGE_PORTAL_RESOURCES_LOCATION = "fr.paris.lutece.portal.resources.{0}_messages";
73      private static final String FORMAT_PACKAGE_PLUGIN_RESOURCES_LOCATION = "fr.paris.lutece.plugins.{0}.resources.{0}_messages";
74      private static final String FORMAT_PACKAGE_MODULE_RESOURCES_LOCATION = "fr.paris.lutece.plugins.{0}.modules.{1}.resources.{1}_messages";
75      private static final Pattern PATTERN_LOCALIZED_KEY = Pattern.compile( "#i18n\\{(.*?)\\}" );
76      private static final String PROPERTY_AVAILABLES_LOCALES = "lutece.i18n.availableLocales";
77      private static final Locale LOCALE_DEFAULT = new Locale( "", "", "" );
78      private static final String PROPERTY_DEFAULT_LOCALE = "lutece.i18n.defaultLocale";
79      private static final String PROPERTY_FORMAT_DATE_SHORT_LIST = "lutece.format.date.short";
80      private static Map<String, String> _pluginBundleNames = Collections.synchronizedMap( new HashMap<String, String>(  ) );
81      private static Map<String, String> _moduleBundleNames = Collections.synchronizedMap( new HashMap<String, String>(  ) );
82      private static Map<String, String> _portalBundleNames = Collections.synchronizedMap( new HashMap<String, String>(  ) );
83      private static final String PROPERTY_PATH_OVERRIDE = "path.i18n.override";
84      private static final ClassLoader _overrideLoader;
85      private static final Map<String, ResourceBundle> _resourceBundleCache = Collections.synchronizedMap( new HashMap<String, ResourceBundle>(  ) );
86  
87      static
88      {
89          File overridePath = null;
90  
91          try
92          {
93              overridePath = new File( AppPathService.getPath( PROPERTY_PATH_OVERRIDE ) );
94          }
95          catch ( AppException e )
96          {
97              // the key is unknown. Message override will be deactivated
98              AppLogService.error( "property " + PROPERTY_PATH_OVERRIDE +
99                  " is undefined. Message overriding will be disabled." );
100         }
101 
102         URL[] overrideURL = null;
103 
104         if ( overridePath != null )
105         {
106             try
107             {
108                 overrideURL = new URL[] { overridePath.toURI(  ).toURL(  ) };
109             }
110             catch ( MalformedURLException e )
111             {
112                 AppLogService.error( "Error initializing message overriding: " + e.getMessage(  ), e );
113             }
114         }
115 
116         if ( overrideURL != null )
117         {
118             _overrideLoader = new URLClassLoader( overrideURL, null );
119         }
120         else
121         {
122             _overrideLoader = null;
123         }
124     }
125 
126     /**
127      * Private constructor
128      */
129     private I18nService(  )
130     {
131     }
132 
133     /**
134      * This method localize a string. It scans for localization keys and replace
135      * them by localized values.<br>
136      * The localization key structure is : #{bundle.key}.<br>
137      * bundle's values should be 'portal' or a plugin name.
138      * @param strSource The string that contains localization keys
139      * @param locale The locale
140      * @return The localized string
141      */
142     public static String localize( String strSource, Locale locale )
143     {
144         String result = strSource;
145 
146         if ( strSource != null )
147         {
148             Matcher matcher = PATTERN_LOCALIZED_KEY.matcher( strSource );
149 
150             if ( matcher.find(  ) )
151             {
152                 StringBuffer sb = new StringBuffer(  );
153 
154                 do
155                 {
156                     matcher.appendReplacement( sb, getLocalizedString( matcher.group( 1 ), locale ) );
157                 }
158                 while ( matcher.find(  ) );
159 
160                 matcher.appendTail( sb );
161                 result = sb.toString(  );
162             }
163         }
164 
165         return result;
166     }
167 
168     /**
169      * Returns the string corresponding to a given key for a given locale <br>
170      * <ul>
171      * <li> Core key structure :<br>
172      * <code>portal.[admin, features, insert, rbac, search, site, style, system, users, util].key</code></li>
173      * <li> Plugin key structure :<br>
174      * <code>[plugin].key </code></li>
175      * <li> Module key structure :<br>
176      * <code>module.[plugin].[module].key </code></li>
177      * </ul>
178      * @param strKey The key of the string
179      * @param theLocale The locale
180      * @return The string corresponding to the key
181      */
182     public static String getLocalizedString( String strKey, Locale theLocale )
183     {
184         Locale locale = theLocale;
185         String strReturn = "";
186 
187         try
188         {
189             int nPos = strKey.indexOf( '.' );
190 
191             if ( nPos != -1 )
192             {
193                 String strBundleKey = strKey.substring( 0, nPos );
194                 String strStringKey = strKey.substring( nPos + 1 );
195 
196                 String strBundle = FORMAT_PACKAGE_PORTAL_RESOURCES_LOCATION;
197 
198                 if ( !strBundleKey.equals( "portal" ) )
199                 {
200                     if ( strBundleKey.equals( "module" ) )
201                     {
202                         // module resource
203                         nPos = strStringKey.indexOf( '.' );
204 
205                         String strPlugin = strStringKey.substring( 0, nPos );
206                         strStringKey = strStringKey.substring( nPos + 1 );
207                         nPos = strStringKey.indexOf( '.' );
208 
209                         String strModule = strStringKey.substring( 0, nPos );
210                         strStringKey = strStringKey.substring( nPos + 1 );
211 
212                         strBundle = getModuleBundleName( strPlugin, strModule );
213                     }
214                     else
215                     {
216                         // plugin resource
217                         strBundle = getPluginBundleName( strBundleKey );
218                     }
219                 }
220                 else
221                 {
222                     nPos = strStringKey.indexOf( '.' );
223 
224                     String strElement = strStringKey.substring( 0, nPos );
225                     strStringKey = strStringKey.substring( nPos + 1 );
226 
227                     strBundle = getPortalBundleName( strElement );
228                 }
229 
230                 // if language is english use a special locale to force using default 
231                 // bundle instead of the bundle of default locale.
232                 if ( locale.getLanguage(  ).equals( Locale.ENGLISH.getLanguage(  ) ) )
233                 {
234                     locale = LOCALE_DEFAULT;
235                 }
236 
237                 ResourceBundle rbLabels = getResourceBundle( locale, strBundle );
238                 strReturn = rbLabels.getString( strStringKey );
239             }
240         }
241         catch ( Exception e )
242         {
243             String strErrorMessage = "Error localizing key : '" + strKey + "' - " + e.getMessage(  );
244 
245             if ( e.getCause(  ) != null )
246             {
247                 strErrorMessage += ( " - cause : " + e.getCause(  ).getMessage(  ) );
248             }
249 
250             AppLogService.error( strErrorMessage );
251         }
252 
253         return strReturn;
254     }
255 
256     /**
257      * Get resource bundle name for plugin
258      * @param strBundleKey the plugin key
259      * @return resource bundle name
260      */
261     private static String getPluginBundleName( String strBundleKey )
262     {
263         String strBundle = _pluginBundleNames.get( strBundleKey );
264 
265         if ( strBundle == null )
266         {
267             Object[] params = { strBundleKey };
268             MessageFormat format = new MessageFormat( FORMAT_PACKAGE_PLUGIN_RESOURCES_LOCATION );
269             strBundle = format.format( params );
270             _pluginBundleNames.put( strBundleKey, strBundle );
271         }
272 
273         return strBundle;
274     }
275 
276     /**
277      * Get resource bundle name for module
278      * @param strPlugin the plugin key
279      * @param strModule the module key
280      * @return resource bundle name
281      */
282     private static String getModuleBundleName( String strPlugin, String strModule )
283     {
284         String key = strPlugin + strModule;
285         String strBundle = _moduleBundleNames.get( key );
286 
287         if ( strBundle == null )
288         {
289             Object[] params = { strPlugin, strModule };
290             MessageFormat format = new MessageFormat( FORMAT_PACKAGE_MODULE_RESOURCES_LOCATION );
291             strBundle = format.format( params );
292             _moduleBundleNames.put( key, strBundle );
293         }
294 
295         return strBundle;
296     }
297 
298     /**
299      * Get resource bundle name for core element
300      * @param strElement element name
301      * @return resource bundle name
302      */
303     private static String getPortalBundleName( String strElement )
304     {
305         String strBundle = _portalBundleNames.get( strElement );
306 
307         if ( strBundle == null )
308         {
309             Object[] params = { strElement };
310             MessageFormat format = new MessageFormat( FORMAT_PACKAGE_PORTAL_RESOURCES_LOCATION );
311             strBundle = format.format( params );
312             _portalBundleNames.put( strElement, strBundle );
313         }
314 
315         return strBundle;
316     }
317 
318     /**
319      * Returns the string corresponding to a given key for a given locale that use a
320      * MessageFormat pattern with arguments.
321      * @return The string corresponding to the key
322      * @param arguments The arguments used as values by the formatter
323      * @param strKey The key of the string that contains the pattern
324      * @param locale The locale
325      */
326     public static String getLocalizedString( String strKey, Object[] arguments, Locale locale )
327     {
328         String strMessagePattern = getLocalizedString( strKey, locale );
329 
330         return MessageFormat.format( strMessagePattern, arguments );
331     }
332 
333     /**
334      * Format a date according to the given locale
335      * @param date The date to format
336      * @param locale The locale
337      * @param nDateFormat A DateFormat constant corresponding to the expected format. (ie: DateFormat.FULL)
338      * @return The formatted date
339      */
340     public static String getLocalizedDate( Date date, Locale locale, int nDateFormat )
341     {
342         DateFormat dateFormatter = DateFormat.getDateInstance( nDateFormat, locale );
343         String strDate = dateFormatter.format( date );
344 
345         return strDate;
346     }
347 
348     /**
349      * Format a date according to the given locale
350      * @param date The date to format
351      * @param locale The locale
352      * @param nDateFormat A DateFormat constant corresponding to the expected format. (ie: DateFormat.FULL)
353      * @param nTimeFormat A TimeFormat constant corresponding to the expected format. (ie: DateFormat.SHORT)
354      * @return The formatted date
355      */
356     public static String getLocalizedDateTime( Date date, Locale locale, int nDateFormat, int nTimeFormat )
357     {
358         DateFormat dateFormatter = DateFormat.getDateTimeInstance( nDateFormat, nTimeFormat, locale );
359         String strDate = dateFormatter.format( date );
360 
361         return strDate;
362     }
363 
364     /**
365      * Returns supported locales for Lutece backoffice
366      * @return A list of locales
367      */
368     public static List<Locale> getAdminAvailableLocales(  )
369     {
370         String strAvailableLocales = AppPropertiesService.getProperty( PROPERTY_AVAILABLES_LOCALES );
371         StringTokenizer strTokens = new StringTokenizer( strAvailableLocales, "," );
372         List<Locale> list = new ArrayList<Locale>(  );
373 
374         while ( strTokens.hasMoreTokens(  ) )
375         {
376             String strLanguage = strTokens.nextToken(  );
377             Locale locale = new Locale( strLanguage );
378             list.add( locale );
379         }
380 
381         return list;
382     }
383 
384     /**
385      * Get the default Locale specified in properties file
386      * @return The default Locale
387      */
388     public static Locale getDefaultLocale(  )
389     {
390         String strDefaultLocale = AppPropertiesService.getProperty( PROPERTY_DEFAULT_LOCALE );
391 
392         return new Locale( strDefaultLocale );
393     }
394 
395     /**
396      * Get the short date format specified by a locale
397      * @param locale The locale
398      * @return The localized short date pattern or null else
399      */
400     public static String getDateFormatShortPattern( Locale locale )
401     {
402         String strAvailableLocales = AppPropertiesService.getProperty( PROPERTY_FORMAT_DATE_SHORT_LIST );
403 
404         if ( ( locale != null ) && ( strAvailableLocales != null ) && !strAvailableLocales.equals( "" ) )
405         {
406             StringTokenizer strTokens = new StringTokenizer( strAvailableLocales, "," );
407             String strToken = null;
408 
409             for ( Locale adminLocale : getAdminAvailableLocales(  ) )
410             {
411                 if ( ( strTokens != null ) && strTokens.hasMoreTokens(  ) )
412                 {
413                     strToken = strTokens.nextToken(  );
414                 }
415 
416                 if ( adminLocale.getLanguage(  ).equals( locale.getLanguage(  ) ) )
417                 {
418                     return strToken;
419                 }
420             }
421         }
422 
423         return null;
424     }
425 
426     /**
427      * Returns a ReferenceList of available locales
428      * @param locale The locale to display available languages
429      * @return A ReferenceList of available locales
430      */
431     public static ReferenceList getAdminLocales( Locale locale )
432     {
433         ReferenceList list = new ReferenceList(  );
434 
435         for ( Locale l : getAdminAvailableLocales(  ) )
436         {
437             list.addItem( l.getLanguage(  ), l.getDisplayLanguage( l ) );
438         }
439 
440         return list;
441     }
442 
443     /**
444      * Localize all items of a list
445      * @param collection The list to localize
446      * @param locale The locale
447      * @return The localized collection
448      */
449     public static Collection localizeCollection( Collection<?extends Localizable> collection, Locale locale )
450     {
451         for ( Localizable object : collection )
452         {
453             object.setLocale( locale );
454         }
455 
456         return collection;
457     }
458 
459     /**
460      * Localize all items of a list
461      * @param list The list to localize
462      * @param locale The locale
463      * @return The localized collection
464      */
465     public static List localizeCollection( List<?extends Localizable> list, Locale locale )
466     {
467         for ( Localizable object : list )
468         {
469             object.setLocale( locale );
470         }
471 
472         return list;
473     }
474 
475     /**
476      * get the resource bundle, possibly with its override
477      * @param locale the locale
478      * @param strBundle the bundle name
479      * @return the resource bundle
480      */
481     private static ResourceBundle getResourceBundle( Locale locale, String strBundle )
482     {
483         String key = strBundle + locale.toString(  );
484         ResourceBundle rbLabels = _resourceBundleCache.get( key );
485 
486         if ( rbLabels == null )
487         {
488             rbLabels = ResourceBundle.getBundle( strBundle, locale );
489 
490             if ( _overrideLoader != null )
491             {
492                 ResourceBundle overrideBundle = null;
493 
494                 try
495                 {
496                     overrideBundle = ResourceBundle.getBundle( strBundle, locale, _overrideLoader );
497                 }
498                 catch ( MissingResourceException e )
499                 {
500                     // no override for this resource
501                     _resourceBundleCache.put( key, rbLabels );
502 
503                     return rbLabels;
504                 }
505 
506                 ResourceBundle res = new CombinedResourceBundle( overrideBundle, rbLabels );
507                 _resourceBundleCache.put( key, res );
508 
509                 return res;
510             }
511 
512             _resourceBundleCache.put( key, rbLabels );
513         }
514 
515         return rbLabels;
516     }
517 
518     /**
519      * Reset the caches
520      * @since 5.1
521      */
522     public static void resetCache(  )
523     {
524         ResourceBundle.clearCache(  );
525 
526         if ( _overrideLoader != null )
527         {
528             ResourceBundle.clearCache( _overrideLoader );
529         }
530 
531         _resourceBundleCache.clear(  );
532     }
533 }