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 > 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 >
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 }