View Javadoc
1   package org.apache.fulcrum.cache.impl;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.ObjectOutputStream;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.concurrent.ConcurrentHashMap;
28  
29  import org.apache.avalon.framework.activity.Disposable;
30  import org.apache.avalon.framework.activity.Initializable;
31  import org.apache.avalon.framework.configuration.Configurable;
32  import org.apache.avalon.framework.configuration.Configuration;
33  import org.apache.avalon.framework.configuration.ConfigurationException;
34  import org.apache.avalon.framework.logger.AbstractLogEnabled;
35  import org.apache.avalon.framework.thread.ThreadSafe;
36  import org.apache.fulcrum.cache.CachedObject;
37  import org.apache.fulcrum.cache.GlobalCacheService;
38  import org.apache.fulcrum.cache.ObjectExpiredException;
39  import org.apache.fulcrum.cache.RefreshableCachedObject;
40  
41  /**
42   * This Service functions as a Global Cache. A global cache is a good place to
43   * store items that you may need to access often but don't necessarily need (or
44   * want) to fetch from the database everytime. A good example would be a look up
45   * table of States that you store in a database and use throughout your
46   * application. Since information about States doesn't change very often, you
47   * could store this information in the Global Cache and decrease the overhead of
48   * hitting the database everytime you need State information.
49   *
50   * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
51   * @author <a href="mailto:jon@clearink.com">Jon S. Stevens</a>
52   * @author <a href="mailto:john@zenplex.com">John Thorhauer</a>
53   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
54   * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
55   * @author <a href="mailto:peter@courefreshableCachedObjectux.biz">Peter CourefreshableCachedObjectux</a>
56   * @version $Id$
57   */
58  public class DefaultGlobalCacheService extends AbstractLogEnabled implements
59          GlobalCacheService, Runnable, Configurable, Initializable, Disposable,
60          ThreadSafe
61  {
62      /**
63       * Initial size of hash table Value must be &gt; 0. Default = 20
64       */
65      public static final int DEFAULT_INITIAL_CACHE_SIZE = 20;
66  
67      /**
68       * The property for the InitalCacheSize
69       */
70      public static final String INITIAL_CACHE_SIZE = "cacheInitialSize";
71  
72      /**
73       * The property for the Cache check frequency
74       */
75      public static final String CACHE_CHECK_FREQUENCY = "cacheCheckFrequency";
76  
77      /**
78       * Cache check frequency in Millis (1000 Millis = 1 second). Value must be &gt;
79       * 0. Default = 5 seconds
80       */
81      public static final long DEFAULT_CACHE_CHECK_FREQUENCY = 5000; // 5 seconds
82  
83      /** The cache. */
84      protected transient ConcurrentHashMap<String, CachedObject<?>> cache = null;
85  
86      /**
87       * cacheCheckFrequency (default - 5 seconds)
88       */
89      private transient long cacheCheckFrequency;
90  
91      /**
92       * cacheInitialSize (default - 20)
93       */
94      private transient int cacheInitialSize;
95  
96      /** thread for removing stale items from the cache */
97      private transient Thread houseKeepingThread;
98  
99      /** flag to stop the housekeeping thread when the component is disposed. */
100     private transient boolean continueThread;
101     
102     public DefaultGlobalCacheService()
103     {
104      
105     }
106 
107     /**
108      * Get the Cache Check Frequency in milliseconds
109      *
110      * @return the time between two cache check runs in milliseconds
111      */
112     public long getCacheCheckFrequency()
113     {
114         return this.cacheCheckFrequency;
115     }
116 
117     /**
118      * Returns an item from the cache. /** Returns an item from the cache.
119      * RefreshableCachedObject will be refreshed if it is expired and not
120      * untouched.
121      *
122      * @param objectId
123      *            The key of the stored object.
124      * @return The object from the cache.
125      * @throws ObjectExpiredException
126      *                when either the object is not in the cache or it has
127      *                expired.
128      */
129     @Override
130     public <T> CachedObject<T> getObject(String objectId) throws ObjectExpiredException
131     {
132         @SuppressWarnings("unchecked")
133         final CachedObject<T> cachedObject = (CachedObject<T>) this.cache.get(objectId);
134         if (cachedObject == null)
135         {
136             // Not in the cache.
137             throw new ObjectExpiredException();
138         }
139         if (cachedObject.isStale())
140         {
141             if (cachedObject instanceof RefreshableCachedObject)
142             {
143                 RefreshableCachedObject<?> refreshableCachedObj = (RefreshableCachedObject<?>) cachedObject;
144                 if (refreshableCachedObj.isUntouched())
145                 {
146                     throw new ObjectExpiredException();
147                 }
148                 // Refresh Object
149                 refreshableCachedObj.refresh();
150                 if (refreshableCachedObj.isStale())
151                 {
152                     throw new ObjectExpiredException();
153                 }
154             }
155             else
156             {
157                 // Expired.
158                 throw new ObjectExpiredException();
159             }
160         }
161         if (cachedObject instanceof RefreshableCachedObject)
162         {
163             // notify it that it's being accessed.
164             RefreshableCachedObject<?> refreshableCachedObj = (RefreshableCachedObject<?>) cachedObject;
165             refreshableCachedObj.touch();
166         }
167         return cachedObject;
168     }
169 
170     /**
171      * Adds an object to the cache.
172      *
173      * @param objectId
174      *            The key to store the object by.
175      * @param object
176      *            The object to cache.
177      */
178     @Override
179     public <T> void addObject(final String objectId, final CachedObject<T> object)
180     {
181         // If the cache already contains the key, remove it and add
182         // the fresh one.
183         if (this.cache.containsKey(objectId))
184         {
185             this.cache.remove(objectId);
186         }
187         this.cache.put(objectId, object);
188     }
189 
190     /**
191      * Removes an object from the cache.
192      *
193      * @param objectId
194      *            The String id for the object.
195      */
196     @Override
197     public void removeObject(String objectId)
198     {
199         this.cache.remove(objectId);
200     }
201 
202     /**
203      * Returns a copy of keys to objects in the cache as a list.
204      *
205      * Note that keys to expired objects are not returned.
206      *
207      * @return A List of <code>String</code>'s representing the keys to
208      *         objects in the cache.
209      */
210     @Override
211     public List<String> getKeys()
212     {
213         ArrayList<String> keys = new ArrayList<>(this.cache.size());
214         for (String key : this.cache.keySet())
215         {
216             try
217             {
218             	getObject(key);
219             }
220             catch (ObjectExpiredException oee)
221             {
222                 // this is OK we just do not want this key
223                 continue;
224             }
225             keys.add(key);
226         }
227         return keys;
228     }
229 
230     /**
231      * Returns a copy of the non-expired CachedObjects in the cache as a list.
232      *
233      * @return A List of <code>CachedObject</code> objects held in the cache
234      */
235     @Override
236     public List<CachedObject<?>> getCachedObjects()
237     {
238         final ArrayList<CachedObject<?>> objects = new ArrayList<>(this.cache.size());
239         for (String key : this.cache.keySet())
240         {
241             CachedObject<?> cachedObject = null;
242             try
243             {
244             	// only add non-null objects
245                 cachedObject = getObject(key);
246                 if ( cachedObject != null )
247                 {
248                 	objects.add(cachedObject);
249                 }
250             }
251             catch (ObjectExpiredException oee)
252             {
253                 // this is OK we just do not want this object
254                 continue;
255             }
256         }
257         return objects;
258     }
259 
260     /**
261      * Circle through the cache and remove stale objects. Frequency is
262      * determined by the cacheCheckFrequency property.
263      */
264     @Override
265     public void run()
266     {
267         while (this.continueThread)
268         {
269             // Sleep for amount of time set in cacheCheckFrequency -
270             // default = 5 seconds.
271             synchronized (this)
272             {
273                 try
274                 {
275                     wait(this.cacheCheckFrequency);
276                 }
277                 catch (InterruptedException exc)
278                 {
279                     // to be expected
280                 }
281             }
282 
283             clearCache();
284         }
285     }
286 
287     /**
288      * Iterate through the cache and remove or refresh stale objects.
289      */
290     public void clearCache()
291     {
292         List<String> refreshThese = new ArrayList<>(20);
293         // Sync on this object so that other threads do not
294         // change the Hashtable while enumerating over it.
295         for (String key : this.cache.keySet())
296         {
297             final CachedObject<?> cachedObject = this.cache.get(key);
298             if (cachedObject instanceof RefreshableCachedObject)
299             {
300                 RefreshableCachedObject<?> refreshableObject = (RefreshableCachedObject<?>) cachedObject;
301                 if (refreshableObject.isUntouched())
302                 {
303                     this.cache.remove(key);
304                 }
305                 else if (refreshableObject.isStale())
306                 {
307                     // to prolong holding the lock on this object
308                     refreshThese.add(key);
309                 }
310             }
311             else if (cachedObject.isStale())
312             {
313                 this.cache.remove(key);
314             }
315         }
316 
317         for (String key : refreshThese)
318         {
319             CachedObject<?> cachedObject = this.cache.get(key);
320             RefreshableCachedObject<?> refreshableCachedObject = (RefreshableCachedObject<?>) cachedObject;
321             refreshableCachedObject.refresh();
322         }
323     }
324 
325     /**
326      * Returns the number of objects currently stored in the cache
327      *
328      * @return int number of object in the cache
329      */
330     @Override
331     public int getNumberOfObjects()
332     {
333         return this.cache.size();
334     }
335 
336     /**
337      * Returns the current size of the cache.
338      *
339      * @return int representing current cache size in number of bytes
340      */
341     @Override
342     public int getCacheSize() throws IOException
343     {
344         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
345         final ObjectOutputStream out = new ObjectOutputStream(baos);
346         out.writeObject(this.cache);
347         out.flush();
348         //
349         // Subtract 4 bytes from the length, because the serialization
350         // magic number (2 bytes) and version number (2 bytes) are
351         // both written to the stream before the object
352         //
353         return baos.toByteArray().length - 4;
354     }
355 
356     /**
357      * Flush the cache of all objects.
358      */
359     @Override
360     public void flushCache()
361     {
362         this.cache.clear();
363     }
364 
365     // ---------------- Avalon Lifecycle Methods ---------------------
366     /**
367      * Avalon component lifecycle method
368      */
369     @Override
370     public void configure(Configuration conf) throws ConfigurationException
371     {
372         this.cacheCheckFrequency = conf.getAttributeAsLong(
373                 CACHE_CHECK_FREQUENCY, DEFAULT_CACHE_CHECK_FREQUENCY);
374         this.cacheInitialSize = conf.getAttributeAsInteger(INITIAL_CACHE_SIZE,
375                 DEFAULT_INITIAL_CACHE_SIZE);
376     }
377 
378     /**
379      * Avalon component lifecycle method
380      */
381     @Override
382     public void initialize() throws Exception
383     {
384         this.cache = new ConcurrentHashMap<>(this.cacheInitialSize);
385         // Start housekeeping thread.
386         this.continueThread = true;
387         this.houseKeepingThread = new Thread(this);
388         // Indicate that this is a system thread. JVM will quit only when
389         // there are no more active user threads. Settings threads spawned
390         // internally by Turbine as daemons allows commandline applications
391         // using Turbine to terminate in an orderly manner.
392         this.houseKeepingThread.setDaemon(true);
393         this.houseKeepingThread.start();
394     }
395 
396     /**
397      * Avalon component lifecycle method
398      */
399     @Override
400     public void dispose()
401     {
402         synchronized (this)
403         {
404             this.continueThread = false;
405             notifyAll();
406         }
407     }
408 }