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.cache;
35  
36  import java.io.File;
37  import java.io.FileInputStream;
38  import java.io.FileNotFoundException;
39  import java.lang.management.ManagementFactory;
40  import java.util.ArrayList;
41  import java.util.List;
42  import java.util.Properties;
43  
44  import javax.management.MBeanServer;
45  
46  import fr.paris.lutece.portal.service.datastore.DatastoreService;
47  import fr.paris.lutece.portal.service.util.AppLogService;
48  import fr.paris.lutece.portal.service.util.AppPathService;
49  import fr.paris.lutece.portal.service.util.AppPropertiesService;
50  import net.sf.ehcache.Cache;
51  import net.sf.ehcache.CacheManager;
52  import net.sf.ehcache.config.CacheConfiguration;
53  import net.sf.ehcache.config.Configuration;
54  import net.sf.ehcache.config.ConfigurationFactory;
55  import net.sf.ehcache.management.ManagementService;
56  
57  /**
58   * Provides cache object for cacheable services
59   */
60  public final class CacheService
61  {
62      private static final String ERROR_NUMERIC_PROP = "Invalid numeric property : {} {} = {}";
63      private static final String PROPERTY_PATH_CONF = "path.conf";
64      private static final String PROPERTY_IS_ENABLED = ".enabled";
65      private static final String FILE_CACHES_STATUS = "caches.dat";
66  
67      // Cache configuration properties
68      private static final String PROPERTY_MAX_ELEMENTS = ".maxElementsInMemory";
69      private static final String PROPERTY_ETERNAL = ".eternal";
70      private static final String PROPERTY_TIME_TO_IDLE = ".timeToIdleSeconds";
71      private static final String PROPERTY_TIME_TO_LIVE = ".timeToLiveSeconds";
72      private static final String PROPERTY_OVERFLOW_TO_DISK = ".overflowToDisk";
73      private static final String PROPERTY_DISK_PERSISTENT = ".diskPersistent";
74      private static final String PROPERTY_DISK_EXPIRY = ".diskExpiryThreadIntervalSeconds";
75      private static final String PROPERTY_MAX_ELEMENTS_DISK = ".maxElementsOnDisk";
76      private static final String PROPERTY_STATISTICS = ".statistics";
77  
78      // Datastore
79      private static final String KEY_PREFIX = "core.cache.status.";
80  
81      // JMX monitoring properties
82      private static final String PROPERTY_JMX_MONITORING = "lutece.cache.jmx.monitoring.enabled";
83      private static final String PROPERTY_MONITOR_CACHE_MANAGER = "lutece.cache.jmx.monitorCacheManager";
84      private static final String PROPERTY_MONITOR_CACHES = "lutece.cache.jmx.monitorCaches";
85      private static final String PROPERTY_MONITOR_CACHE_CONFIGURATIONS = "lutece.cache.jmx.monitorCacheConfiguration";
86      private static final String PROPERTY_MONITOR_CACHE_STATISTICS = "lutece.cache.jmx.monitorCacheStatistics";
87      private static final String FALSE = "false";
88      private static final String TRUE = "true";
89      private static final String ENABLED = "1";
90      private static final String DISABLED = "0";
91      private static final String NOT_FOUND = "NOT FOUND";
92      private static final String PREFIX_DEFAULT = "lutece.cache.default";
93      private static final String LUTECE_CACHEMANAGER_NAME = "LuteceCacheManager";
94      private static CacheService _singleton;
95      private static CacheManager _manager;
96  
97      private static List<CacheableService> _listCacheableServicesRegistry = new ArrayList<>( );
98      private int _nDefaultMaxElementsInMemory;
99      private boolean _bDefaultEternal;
100     private long _lDefaultTimeToIdle;
101     private long _lDefaultTimeToLive;
102     private boolean _bDefaultOverflowToDisk;
103     private boolean _bDefaultDiskPersistent;
104     private long _lDefaultDiskExpiry;
105     private int _nDefaultMaxElementsOnDisk;
106     private boolean _bDefaultStatistics;
107 
108     /**
109      * Creates a new instance of CacheService
110      */
111     private CacheService( )
112     {
113     }
114 
115     /**
116      * Gets the unique instance of the CacheService
117      *
118      * @return The unique instance of the CacheService
119      */
120     public static synchronized CacheService getInstance( )
121     {
122         if ( _singleton == null )
123         {
124             _singleton = new CacheService( );
125             _singleton.init( );
126             Configuration configuration = ConfigurationFactory.parseConfiguration( );
127             configuration.setName( LUTECE_CACHEMANAGER_NAME );
128             _manager = CacheManager.create( configuration );
129         }
130 
131         return _singleton;
132     }
133 
134     /**
135      * Itializes the service by creating a manager object with a given configuration file.
136      */
137     private void init( )
138     {
139         loadDefaults( );
140         loadCachesConfig( );
141 
142         boolean bJmxMonitoring = AppPropertiesService.getProperty( PROPERTY_JMX_MONITORING, FALSE ).equals( TRUE );
143 
144         if ( bJmxMonitoring )
145         {
146             initJmxMonitoring( );
147         }
148     }
149 
150     /**
151      * Init JMX monitoring configuration
152      */
153     private void initJmxMonitoring( )
154     {
155         MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer( );
156 
157         boolean bRegisterCacheManager = AppPropertiesService.getProperty( PROPERTY_MONITOR_CACHE_MANAGER, FALSE ).equals( TRUE );
158         boolean bRegisterCaches = AppPropertiesService.getProperty( PROPERTY_MONITOR_CACHES, FALSE ).equals( TRUE );
159         boolean bRegisterCacheConfigurations = AppPropertiesService.getProperty( PROPERTY_MONITOR_CACHE_CONFIGURATIONS, FALSE ).equals( TRUE );
160         boolean bRegisterCacheStatistics = AppPropertiesService.getProperty( PROPERTY_MONITOR_CACHE_STATISTICS, FALSE ).equals( TRUE );
161         ManagementService.registerMBeans( _manager, mBeanServer, bRegisterCacheManager, bRegisterCaches, bRegisterCacheConfigurations,
162                 bRegisterCacheStatistics );
163     }
164 
165     /**
166      * Create a cache for a given Service
167      *
168      * @param strCacheName
169      *            The Cache/Service name
170      * @return A cache object
171      */
172     public Cache createCache( String strCacheName )
173     {
174         Cache cache = new Cache( getCacheConfiguration( strCacheName ) );
175         _manager.addCache( cache );
176 
177         return _manager.getCache( strCacheName );
178     }
179 
180     /**
181      * Reset all caches
182      */
183     public static void resetCaches( )
184     {
185         // Reset cache
186         for ( CacheableService cs : _listCacheableServicesRegistry )
187         {
188             cs.resetCache( );
189         }
190     }
191 
192     /**
193      * Shutdown the cache service and the cache manager. Should be called when the webapp is stopped.
194      */
195     public void shutdown( )
196     {
197         CacheService.storeCachesStatus( );
198         _manager.shutdown( );
199     }
200 
201     /**
202      * Registers a new CacheableService
203      *
204      * @param cs
205      *            The CacheableService
206      */
207     public static void registerCacheableService( CacheableService cs )
208     {
209         _listCacheableServicesRegistry.add( cs );
210 
211         // read cache status from file "caches.dat"
212         cs.enableCache( getStatus( cs ) );
213     }
214 
215     /**
216      * Returns all registered Cacheable services
217      *
218      * @return A collection containing all registered Cacheable services
219      */
220     public static List<CacheableService> getCacheableServicesList( )
221     {
222         return _listCacheableServicesRegistry;
223     }
224 
225     /**
226      * Stores cache status
227      */
228     public static void storeCachesStatus( )
229     {
230         for ( CacheableService cs : _listCacheableServicesRegistry )
231         {
232             String strKey = getDSKey( cs.getName( ), PROPERTY_IS_ENABLED );
233             DatastoreService.setInstanceDataValue( strKey, cs.isCacheEnable( ) ? ENABLED : DISABLED );
234         }
235     }
236 
237     /**
238      * Returns cache config
239      *
240      * @param cache
241      *            The cache
242      * @return Cache infos
243      */
244     static String getInfos( Cache cache )
245     {
246         StringBuilder sbInfos = new StringBuilder( );
247         sbInfos.append( PROPERTY_MAX_ELEMENTS ).append( "=" ).append( cache.getCacheConfiguration( ).getMaxElementsInMemory( ) ).append( "\n" );
248         sbInfos.append( PROPERTY_ETERNAL ).append( "=" ).append( cache.getCacheConfiguration( ).isEternal( ) ).append( "\n" );
249         sbInfos.append( PROPERTY_TIME_TO_IDLE ).append( "=" ).append( cache.getCacheConfiguration( ).getTimeToIdleSeconds( ) ).append( "\n" );
250         sbInfos.append( PROPERTY_TIME_TO_LIVE ).append( "=" ).append( cache.getCacheConfiguration( ).getTimeToLiveSeconds( ) ).append( "\n" );
251         sbInfos.append( PROPERTY_OVERFLOW_TO_DISK ).append( "=" ).append( cache.getCacheConfiguration( ).isOverflowToDisk( ) ).append( "\n" );
252         sbInfos.append( PROPERTY_DISK_PERSISTENT ).append( "=" ).append( cache.getCacheConfiguration( ).isDiskPersistent( ) ).append( "\n" );
253         sbInfos.append( PROPERTY_DISK_EXPIRY ).append( "=" ).append( cache.getCacheConfiguration( ).getDiskExpiryThreadIntervalSeconds( ) ).append( "\n" );
254         sbInfos.append( PROPERTY_MAX_ELEMENTS_DISK ).append( "=" ).append( cache.getCacheConfiguration( ).getMaxElementsOnDisk( ) ).append( "\n" );
255         sbInfos.append( PROPERTY_STATISTICS ).append( '=' ).append( cache.getCacheConfiguration( ).getStatistics( ) ).append( "\n" );
256 
257         return sbInfos.toString( );
258     }
259 
260     /**
261      * Load defaults configuration parameters
262      */
263     private void loadDefaults( )
264     {
265         _nDefaultMaxElementsInMemory = AppPropertiesService.getPropertyInt( PREFIX_DEFAULT + PROPERTY_MAX_ELEMENTS, 10000 );
266         _bDefaultEternal = AppPropertiesService.getPropertyBoolean( PREFIX_DEFAULT + PROPERTY_ETERNAL, false );
267         _lDefaultTimeToIdle = AppPropertiesService.getPropertyLong( PREFIX_DEFAULT + PROPERTY_TIME_TO_IDLE, 10000L );
268         _lDefaultTimeToLive = AppPropertiesService.getPropertyLong( PREFIX_DEFAULT + PROPERTY_TIME_TO_LIVE, 10000L );
269         _bDefaultOverflowToDisk = AppPropertiesService.getPropertyBoolean( PREFIX_DEFAULT + PROPERTY_OVERFLOW_TO_DISK, true );
270         _bDefaultDiskPersistent = AppPropertiesService.getPropertyBoolean( PREFIX_DEFAULT + PROPERTY_DISK_PERSISTENT, true );
271         _lDefaultDiskExpiry = AppPropertiesService.getPropertyLong( PREFIX_DEFAULT + PROPERTY_DISK_EXPIRY, 120L );
272         _nDefaultMaxElementsOnDisk = AppPropertiesService.getPropertyInt( PREFIX_DEFAULT + PROPERTY_MAX_ELEMENTS_DISK, 10000 );
273         _bDefaultStatistics = AppPropertiesService.getPropertyBoolean( PREFIX_DEFAULT + PROPERTY_STATISTICS, false );
274     }
275 
276     /**
277      * Load caches status
278      */
279     private void loadCachesConfig( )
280     {
281         String strCachesStatusFile = AppPathService.getPath( PROPERTY_PATH_CONF, FILE_CACHES_STATUS );
282         File file = new File( strCachesStatusFile );
283 
284         try ( FileInputStream fis = new FileInputStream( file ) )
285         {
286             Properties properties = new Properties( );
287             properties.load( fis );
288 
289             // If the keys aren't found in the datastore then create a key in it
290             for ( String strKey : properties.stringPropertyNames( ) )
291             {
292                 String strDSKey = KEY_PREFIX + strKey;
293 
294                 if ( !DatastoreService.existsInstanceKey( strDSKey ) )
295                 {
296                     String strValue = properties.getProperty( strKey );
297                     DatastoreService.setInstanceDataValue( strDSKey, strValue );
298                 }
299             }
300         }
301         catch( FileNotFoundException e )
302         {
303             AppLogService.error( "No cache.dat file. Should be created at shutdown." );
304         }
305         catch( Exception e )
306         {
307             AppLogService.error( "Error loading caches status defined in file : {}", file.getAbsolutePath( ), e );
308         }
309     }
310 
311     /**
312      * Update cache status
313      * 
314      * @param cs
315      *            Cacheable Service
316      */
317     public static void updateCacheStatus( CacheableService cs )
318     {
319         String strKey = getDSKey( cs.getName( ), PROPERTY_IS_ENABLED );
320         DatastoreService.setInstanceDataValue( strKey, ( cs.isCacheEnable( ) ? ENABLED : DISABLED ) );
321     }
322 
323     /**
324      * Returns the cache status
325      *
326      * @param cs
327      *            The cacheable service
328      * @return The status
329      */
330     private static boolean getStatus( CacheableService cs )
331     {
332         String strEnabled = DatastoreService.getInstanceDataValue( getDSKey( cs.getName( ), PROPERTY_IS_ENABLED ), DISABLED );
333 
334         return strEnabled.equals( ENABLED );
335     }
336 
337     /**
338      * Return the key of a datastore property
339      * 
340      * @param strCacheName
341      *            The cacheable service
342      * @param strProperty
343      *            The property
344      * @return The DS key
345      */
346     private static String getDSKey( String strCacheName, String strProperty )
347     {
348         return KEY_PREFIX + normalizeName( strCacheName ) + strProperty;
349     }
350 
351     /**
352      * Normalize name (remove spaces)
353      *
354      * @param strName
355      *            The name to normalize
356      * @return The normalized name
357      */
358     private static String normalizeName( String strName )
359     {
360         return strName.replace( " ", "" );
361     }
362 
363     /**
364      * Read cache config from the file caches.dat
365      *
366      * @param strCacheName
367      *            The cache name
368      * @return The config
369      */
370     private CacheConfiguration getCacheConfiguration( String strCacheName )
371     {
372         CacheConfiguration config = new CacheConfiguration( );
373         config.setName( strCacheName );
374         config.setMaxElementsInMemory( getIntProperty( strCacheName, PROPERTY_MAX_ELEMENTS, _nDefaultMaxElementsInMemory ) );
375         config.setEternal( getBooleanProperty( strCacheName, PROPERTY_ETERNAL, _bDefaultEternal ) );
376         config.setTimeToIdleSeconds( getLongProperty( strCacheName, PROPERTY_TIME_TO_IDLE, _lDefaultTimeToIdle ) );
377         config.setTimeToLiveSeconds( getLongProperty( strCacheName, PROPERTY_TIME_TO_LIVE, _lDefaultTimeToLive ) );
378         config.setOverflowToDisk( getBooleanProperty( strCacheName, PROPERTY_OVERFLOW_TO_DISK, _bDefaultOverflowToDisk ) );
379         config.setDiskPersistent( getBooleanProperty( strCacheName, PROPERTY_DISK_PERSISTENT, _bDefaultDiskPersistent ) );
380         config.setDiskExpiryThreadIntervalSeconds( getLongProperty( strCacheName, PROPERTY_DISK_EXPIRY, _lDefaultDiskExpiry ) );
381         config.setMaxElementsOnDisk( getIntProperty( strCacheName, PROPERTY_MAX_ELEMENTS_DISK, _nDefaultMaxElementsOnDisk ) );
382         config.setStatistics( getBooleanProperty( strCacheName, PROPERTY_STATISTICS, _bDefaultStatistics ) );
383 
384         return config;
385     }
386 
387     /**
388      * Read an Integer property
389      *
390      * @param strCacheName
391      *            Property's prefix
392      * @param strProperty
393      *            the key
394      * @param nDefault
395      *            the default value
396      * @return The property's value
397      */
398     private int getIntProperty( String strCacheName, String strProperty, int nDefault )
399     {
400         String strKey = getDSKey( strCacheName, strProperty );
401 
402         if ( DatastoreService.existsInstanceKey( strKey ) )
403         {
404             String strValue = NOT_FOUND;
405 
406             try
407             {
408                 strValue = DatastoreService.getInstanceDataValue( strKey, strValue );
409 
410                 return Integer.parseInt( strValue );
411             }
412             catch( NumberFormatException e )
413             {
414                 AppLogService.error( ERROR_NUMERIC_PROP, strCacheName, strProperty, strValue, e );
415             }
416         }
417 
418         return nDefault;
419     }
420 
421     /**
422      * Read a Long property
423      *
424      * @param strCacheName
425      *            Property's prefix
426      * @param strProperty
427      *            the key
428      * @param lDefault
429      *            the default value
430      * @return The property's value
431      */
432     private long getLongProperty( String strCacheName, String strProperty, long lDefault )
433     {
434         String strKey = getDSKey( strCacheName, strProperty );
435 
436         if ( DatastoreService.existsInstanceKey( strKey ) )
437         {
438             String strValue = NOT_FOUND;
439 
440             try
441             {
442                 strValue = DatastoreService.getInstanceDataValue( strKey, strValue );
443 
444                 return Integer.parseInt( strValue );
445             }
446             catch( NumberFormatException e )
447             {
448                 AppLogService.error( ERROR_NUMERIC_PROP, strCacheName, strProperty, strValue, e );
449             }
450         }
451 
452         return lDefault;
453     }
454 
455     /**
456      * Read a Boolean property
457      *
458      * @param strCacheName
459      *            Property's prefix
460      * @param strProperty
461      *            the key
462      * @param bDefault
463      *            the default value
464      * @return The property's value
465      */
466     private boolean getBooleanProperty( String strCacheName, String strProperty, boolean bDefault )
467     {
468         String strKey = getDSKey( strCacheName, strProperty );
469 
470         if ( DatastoreService.existsInstanceKey( strKey ) )
471         {
472             String strValue = DatastoreService.getInstanceDataValue( strKey, NOT_FOUND );
473 
474             return ( strValue.equalsIgnoreCase( TRUE ) );
475         }
476 
477         return bDefault;
478     }
479 }