View Javadoc
1   /*
2    * Copyright (c) 2002-2022, City of 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   * This class provides services for internationalization (i18n)
67   * 
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 {} is undefined. Message overriding will be disabled.", PROPERTY_PATH_OVERRIDE );
99          }
100 
101         URL [ ] overrideURL = null;
102 
103         if ( overridePath != null )
104         {
105             try
106             {
107                 overrideURL = new URL [ ] {
108                         overridePath.toURI( ).toURL( )
109                 };
110             }
111             catch( MalformedURLException e )
112             {
113                 AppLogService.error( "Error initializing message overriding: {}", e.getMessage( ), e );
114             }
115         }
116 
117         if ( overrideURL != null )
118         {
119             _overrideLoader = new URLClassLoader( overrideURL, null );
120         }
121         else
122         {
123             _overrideLoader = null;
124         }
125     }
126 
127     /**
128      * Private constructor
129      */
130     private I18nService( )
131     {
132     }
133 
134     /**
135      * This method localize a string. It scans for localization keys and replace 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      * 
139      * @param strSource
140      *            The string that contains localization keys
141      * @param locale
142      *            The locale
143      * @return The localized string
144      */
145     public static String localize( String strSource, Locale locale )
146     {
147         String result = strSource;
148 
149         if ( strSource != null )
150         {
151             Matcher matcher = PATTERN_LOCALIZED_KEY.matcher( strSource );
152 
153             if ( matcher.find( ) )
154             {
155                 StringBuffer sb = new StringBuffer( );
156 
157                 do
158                 {
159                     matcher.appendReplacement( sb, getLocalizedString( matcher.group( 1 ), locale ) );
160                 }
161                 while ( matcher.find( ) );
162 
163                 matcher.appendTail( sb );
164                 result = sb.toString( );
165             }
166         }
167 
168         return result;
169     }
170 
171     /**
172      * Returns the string corresponding to a given key for a given locale <br>
173      * <ul>
174      * <li>Core key structure :<br>
175      * <code>portal.[admin, features, insert, rbac, search, site, style, system, users, util].key</code></li>
176      * <li>Plugin key structure :<br>
177      * <code>[plugin].key </code></li>
178      * <li>Module key structure :<br>
179      * <code>module.[plugin].[module].key </code></li>
180      * </ul>
181      * 
182      * @param strKey
183      *            The key of the string
184      * @param theLocale
185      *            The locale
186      * @return The string corresponding to the key
187      */
188     public static String getLocalizedString( String strKey, Locale theLocale )
189     {
190         Locale locale = theLocale;
191         String strReturn = "";
192 
193         try
194         {
195             int nPos = strKey.indexOf( '.' );
196 
197             if ( nPos != -1 )
198             {
199                 String strBundleKey = strKey.substring( 0, nPos );
200                 String strStringKey = strKey.substring( nPos + 1 );
201 
202                 String strBundle;
203 
204                 if ( !strBundleKey.equals( "portal" ) )
205                 {
206                     if ( strBundleKey.equals( "module" ) )
207                     {
208                         // module resource
209                         nPos = strStringKey.indexOf( '.' );
210 
211                         String strPlugin = strStringKey.substring( 0, nPos );
212                         strStringKey = strStringKey.substring( nPos + 1 );
213                         nPos = strStringKey.indexOf( '.' );
214 
215                         String strModule = strStringKey.substring( 0, nPos );
216                         strStringKey = strStringKey.substring( nPos + 1 );
217 
218                         strBundle = getModuleBundleName( strPlugin, strModule );
219                     }
220                     else
221                     {
222                         // plugin resource
223                         strBundle = getPluginBundleName( strBundleKey );
224                     }
225                 }
226                 else
227                 {
228                     nPos = strStringKey.indexOf( '.' );
229 
230                     String strElement = strStringKey.substring( 0, nPos );
231                     strStringKey = strStringKey.substring( nPos + 1 );
232 
233                     strBundle = getPortalBundleName( strElement );
234                 }
235 
236                 // if language is english use a special locale to force using default
237                 // bundle instead of the bundle of default locale.
238                 if ( locale.getLanguage( ).equals( Locale.ENGLISH.getLanguage( ) ) )
239                 {
240                     locale = LOCALE_DEFAULT;
241                 }
242 
243                 ResourceBundle rbLabels = getResourceBundle( locale, strBundle );
244                 strReturn = rbLabels.getString( strStringKey );
245             }
246         }
247         catch( Exception e )
248         {
249             String strErrorMessage = "Error localizing key : '" + strKey + "' - " + e.getMessage( );
250 
251             if ( e.getCause( ) != null )
252             {
253                 strErrorMessage += ( " - cause : " + e.getCause( ).getMessage( ) );
254             }
255 
256             AppLogService.error( strErrorMessage );
257         }
258 
259         return strReturn;
260     }
261 
262     /**
263      * Get resource bundle name for plugin
264      * 
265      * @param strBundleKey
266      *            the plugin key
267      * @return resource bundle name
268      */
269     private static String getPluginBundleName( String strBundleKey )
270     {
271         return _pluginBundleNames.computeIfAbsent( strBundleKey, s -> new MessageFormat( FORMAT_PACKAGE_PLUGIN_RESOURCES_LOCATION ).format( new String [ ] {
272                 s
273         } ) );
274     }
275 
276     /**
277      * Get resource bundle name for module
278      * 
279      * @param strPlugin
280      *            the plugin key
281      * @param strModule
282      *            the module key
283      * @return resource bundle name
284      */
285     private static String getModuleBundleName( String strPlugin, String strModule )
286     {
287         String key = strPlugin + strModule;
288         return _moduleBundleNames.computeIfAbsent( key, s -> new MessageFormat( FORMAT_PACKAGE_MODULE_RESOURCES_LOCATION ).format( new String [ ] {
289                 strPlugin, strModule
290         } ) );
291     }
292 
293     /**
294      * Get resource bundle name for core element
295      * 
296      * @param strElement
297      *            element name
298      * @return resource bundle name
299      */
300     private static String getPortalBundleName( String strElement )
301     {
302         return _portalBundleNames.computeIfAbsent( strElement, s -> new MessageFormat( FORMAT_PACKAGE_PORTAL_RESOURCES_LOCATION ).format( new String [ ] {
303                 s
304         } ) );
305     }
306 
307     /**
308      * Returns the string corresponding to a given key for a given locale that use a MessageFormat pattern with arguments.
309      * 
310      * @return The string corresponding to the key
311      * @param arguments
312      *            The arguments used as values by the formatter
313      * @param strKey
314      *            The key of the string that contains the pattern
315      * @param locale
316      *            The locale
317      */
318     public static String getLocalizedString( String strKey, Object [ ] arguments, Locale locale )
319     {
320         String strMessagePattern = getLocalizedString( strKey, locale );
321 
322         return MessageFormat.format( strMessagePattern, arguments );
323     }
324 
325     /**
326      * Format a date according to the given locale
327      * 
328      * @param date
329      *            The date to format
330      * @param locale
331      *            The locale
332      * @param nDateFormat
333      *            A DateFormat constant corresponding to the expected format. (ie: DateFormat.FULL)
334      * @return The formatted date
335      */
336     public static String getLocalizedDate( Date date, Locale locale, int nDateFormat )
337     {
338         DateFormat dateFormatter = DateFormat.getDateInstance( nDateFormat, locale );
339         return dateFormatter.format( date );
340     }
341 
342     /**
343      * Format a date according to the given locale
344      * 
345      * @param date
346      *            The date to format
347      * @param locale
348      *            The locale
349      * @param nDateFormat
350      *            A DateFormat constant corresponding to the expected format. (ie: DateFormat.FULL)
351      * @param nTimeFormat
352      *            A TimeFormat constant corresponding to the expected format. (ie: DateFormat.SHORT)
353      * @return The formatted date
354      */
355     public static String getLocalizedDateTime( Date date, Locale locale, int nDateFormat, int nTimeFormat )
356     {
357         DateFormat dateFormatter = DateFormat.getDateTimeInstance( nDateFormat, nTimeFormat, locale );
358         return dateFormatter.format( date );
359     }
360 
361     /**
362      * Returns supported locales for Lutece backoffice
363      * 
364      * @return A list of locales
365      */
366     public static List<Locale> getAdminAvailableLocales( )
367     {
368         String strAvailableLocales = AppPropertiesService.getProperty( PROPERTY_AVAILABLES_LOCALES );
369         StringTokenizer strTokens = new StringTokenizer( strAvailableLocales, "," );
370         List<Locale> list = new ArrayList<>( );
371 
372         while ( strTokens.hasMoreTokens( ) )
373         {
374             String strLanguage = strTokens.nextToken( );
375             Locale locale = new Locale( strLanguage );
376             list.add( locale );
377         }
378 
379         return list;
380     }
381 
382     /**
383      * Get the default Locale specified in properties file
384      * 
385      * @return The default Locale
386      */
387     public static Locale getDefaultLocale( )
388     {
389         String strDefaultLocale = AppPropertiesService.getProperty( PROPERTY_DEFAULT_LOCALE );
390 
391         return new Locale( strDefaultLocale );
392     }
393 
394     /**
395      * Get the short date format specified by a locale
396      * 
397      * @param locale
398      *            The locale
399      * @return The localized short date pattern or null else
400      */
401     public static String getDateFormatShortPattern( Locale locale )
402     {
403         String strAvailableLocales = AppPropertiesService.getProperty( PROPERTY_FORMAT_DATE_SHORT_LIST );
404 
405         if ( ( locale != null ) && ( strAvailableLocales != null ) && !strAvailableLocales.equals( "" ) )
406         {
407             StringTokenizer strTokens = new StringTokenizer( strAvailableLocales, "," );
408             String strToken = null;
409 
410             for ( Locale adminLocale : getAdminAvailableLocales( ) )
411             {
412                 if ( strTokens.hasMoreTokens( ) )
413                 {
414                     strToken = strTokens.nextToken( );
415                 }
416 
417                 if ( adminLocale.getLanguage( ).equals( locale.getLanguage( ) ) )
418                 {
419                     return strToken;
420                 }
421             }
422         }
423 
424         return null;
425     }
426 
427     /**
428      * Returns a ReferenceList of available locales
429      * 
430      * @param locale
431      *            The locale to display available languages
432      * @return A ReferenceList of available locales
433      */
434     public static ReferenceList getAdminLocales( Locale locale )
435     {
436         ReferenceListnceList.html#ReferenceList">ReferenceList list = new ReferenceList( );
437 
438         for ( Locale l : getAdminAvailableLocales( ) )
439         {
440             list.addItem( l.getLanguage( ), l.getDisplayLanguage( l ) );
441         }
442 
443         return list;
444     }
445 
446     /**
447      * Localize in place all items of a collection
448      * 
449      * @param <E>
450      *            the type of the elements of the collection
451      * 
452      * @param collection
453      *            The collection to localize
454      * @param locale
455      *            The locale
456      * @return The localized collection passed as argument
457      */
458     public static <E extends Localizable> Collection<E> localizeCollection( Collection<E> collection, Locale locale )
459     {
460         for ( Localizable object : collection )
461         {
462             object.setLocale( locale );
463         }
464 
465         return collection;
466     }
467 
468     /**
469      * Localize in place all items of a list
470      * 
471      * @param <E>
472      *            the type of the elements of the list
473      * 
474      * @param list
475      *            The list to localize
476      * @param locale
477      *            The locale
478      * @return The localized collection passed as argument
479      */
480     public static <E extends Localizable> List<E> localizeCollection( List<E> list, Locale locale )
481     {
482         for ( Localizable object : list )
483         {
484             object.setLocale( locale );
485         }
486 
487         return list;
488     }
489 
490     /**
491      * get the resource bundle, possibly with its override
492      * 
493      * @param locale
494      *            the locale
495      * @param strBundle
496      *            the bundle name
497      * @return the resource bundle
498      */
499     private static ResourceBundle getResourceBundle( Locale locale, String strBundle )
500     {
501         String key = strBundle + locale.toString( );
502         return _resourceBundleCache.computeIfAbsent( key, k -> createResourceBundle( strBundle, locale ) );
503     }
504 
505     private static ResourceBundle createResourceBundle( String strBundle, Locale locale )
506     {
507         ResourceBundle rbLabels = ResourceBundle.getBundle( strBundle, locale );
508 
509         if ( _overrideLoader != null )
510         {
511             ResourceBundle overrideBundle = null;
512 
513             try
514             {
515                 overrideBundle = ResourceBundle.getBundle( strBundle, locale, _overrideLoader );
516             }
517             catch( MissingResourceException e )
518             {
519                 // no override for this resource
520                 return rbLabels;
521             }
522 
523             return new CombinedResourceBundle( overrideBundle, rbLabels );
524         }
525         return rbLabels;
526     }
527 
528     /**
529      * Reset the caches
530      * 
531      * @since 5.1
532      */
533     public static void resetCache( )
534     {
535         ResourceBundle.clearCache( );
536 
537         if ( _overrideLoader != null )
538         {
539             ResourceBundle.clearCache( _overrideLoader );
540         }
541 
542         _resourceBundleCache.clear( );
543     }
544 }