001 package org.apache.fulcrum.cache.impl; 002 003 /* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022 import java.io.ByteArrayOutputStream; 023 import java.io.IOException; 024 import java.io.ObjectOutputStream; 025 import java.util.ArrayList; 026 import java.util.Enumeration; 027 import java.util.Hashtable; 028 import java.util.Iterator; 029 import java.util.List; 030 031 import org.apache.avalon.framework.activity.Disposable; 032 import org.apache.avalon.framework.activity.Initializable; 033 import org.apache.avalon.framework.configuration.Configurable; 034 import org.apache.avalon.framework.configuration.Configuration; 035 import org.apache.avalon.framework.configuration.ConfigurationException; 036 import org.apache.avalon.framework.logger.AbstractLogEnabled; 037 import org.apache.avalon.framework.thread.ThreadSafe; 038 import org.apache.fulcrum.cache.CachedObject; 039 import org.apache.fulcrum.cache.GlobalCacheService; 040 import org.apache.fulcrum.cache.ObjectExpiredException; 041 import org.apache.fulcrum.cache.RefreshableCachedObject; 042 043 /** 044 * This Service functions as a Global Cache. A global cache is a good place to 045 * store items that you may need to access often but don't necessarily need (or 046 * want) to fetch from the database everytime. A good example would be a look up 047 * table of States that you store in a database and use throughout your 048 * application. Since information about States doesn't change very often, you 049 * could store this information in the Global Cache and decrease the overhead of 050 * hitting the database everytime you need State information. 051 * 052 * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a> 053 * @author <a href="mailto:jon@clearink.com">Jon S. Stevens</a> 054 * @author <a href="mailto:john@zenplex.com">John Thorhauer</a> 055 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a> 056 * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a> 057 * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a> 058 * @version $Id: DefaultGlobalCacheService.java 927153 2010-03-24 18:55:08Z tv $ 059 */ 060 public class DefaultGlobalCacheService extends AbstractLogEnabled implements 061 GlobalCacheService, Runnable, Configurable, Initializable, Disposable, 062 ThreadSafe 063 { 064 /** 065 * Initial size of hash table Value must be > 0. Default = 20 066 */ 067 public static final int DEFAULT_INITIAL_CACHE_SIZE = 20; 068 069 /** 070 * The property for the InitalCacheSize 071 */ 072 public static final String INITIAL_CACHE_SIZE = "cacheInitialSize"; 073 074 /** 075 * The property for the Cache check frequency 076 */ 077 public static final String CACHE_CHECK_FREQUENCY = "cacheCheckFrequency"; 078 079 /** 080 * Cache check frequency in Millis (1000 Millis = 1 second). Value must be > 081 * 0. Default = 5 seconds 082 */ 083 public static final long DEFAULT_CACHE_CHECK_FREQUENCY = 5000; // 5 seconds 084 085 /** The cache. * */ 086 protected Hashtable cache = null; 087 088 /** 089 * cacheCheckFrequency (default - 5 seconds) 090 */ 091 private long cacheCheckFrequency; 092 093 /** 094 * cacheInitialSize (default - 20) 095 */ 096 private int cacheInitialSize; 097 098 /** thread for removing stale items from the cache */ 099 private Thread housekeeping; 100 101 /** flag to stop the housekeeping thread when the component is disposed. */ 102 private boolean continueThread; 103 104 /** 105 * Get the Cache Check Frequency in milliseconds 106 * 107 * @return the time between two cache check runs in milliseconds 108 */ 109 public long getCacheCheckFrequency() 110 { 111 return this.cacheCheckFrequency; 112 } 113 114 /** 115 * Returns an item from the cache. /** Returns an item from the cache. 116 * RefreshableCachedObject will be refreshed if it is expired and not 117 * untouched. 118 * 119 * @param id 120 * The key of the stored object. 121 * @return The object from the cache. 122 * @exception ObjectExpiredException, 123 * when either the object is not in the cache or it has 124 * expired. 125 */ 126 public CachedObject getObject(String id) throws ObjectExpiredException 127 { 128 CachedObject obj = null; 129 obj = (CachedObject) this.cache.get(id); 130 if (obj == null) 131 { 132 // Not in the cache. 133 throw new ObjectExpiredException(); 134 } 135 if (obj.isStale()) 136 { 137 if (obj instanceof RefreshableCachedObject) 138 { 139 RefreshableCachedObject rco = (RefreshableCachedObject) obj; 140 if (rco.isUntouched()) 141 { 142 throw new ObjectExpiredException(); 143 } 144 // Refresh Object 145 rco.refresh(); 146 if (rco.isStale()) 147 { 148 throw new ObjectExpiredException(); 149 } 150 } 151 else 152 { 153 // Expired. 154 throw new ObjectExpiredException(); 155 } 156 } 157 if (obj instanceof RefreshableCachedObject) 158 { 159 // notify it that it's being accessed. 160 RefreshableCachedObject rco = (RefreshableCachedObject) obj; 161 rco.touch(); 162 } 163 return obj; 164 } 165 166 /** 167 * Adds an object to the cache. 168 * 169 * @param id 170 * The key to store the object by. 171 * @param o 172 * The object to cache. 173 */ 174 public void addObject(String id, CachedObject o) 175 { 176 // If the cache already contains the key, remove it and add 177 // the fresh one. 178 if (this.cache.containsKey(id)) 179 { 180 this.cache.remove(id); 181 } 182 this.cache.put(id, o); 183 } 184 185 /** 186 * Removes an object from the cache. 187 * 188 * @param id 189 * The String id for the object. 190 */ 191 public void removeObject(String id) 192 { 193 this.cache.remove(id); 194 } 195 196 /** 197 * Returns a copy of keys to objects in the cache as a list. 198 * 199 * Note that keys to expired objects are not returned. 200 * 201 * @return A List of <code>String</code>'s representing the keys to 202 * objects in the cache. 203 */ 204 public List getKeys() 205 { 206 ArrayList keys = new ArrayList(this.cache.size()); 207 synchronized (this) 208 { 209 for (Iterator itr = this.cache.keySet().iterator(); itr.hasNext();) 210 { 211 String key = (String) itr.next(); 212 try 213 { 214 /* CachedObject obj = */getObject(key); 215 } 216 catch (ObjectExpiredException oee) 217 { 218 // this is OK we just do not want this key 219 continue; 220 } 221 keys.add(new String(key)); 222 } 223 } 224 return keys; 225 } 226 227 /** 228 * Returns a copy of the non-expired CachedObjects in the cache as a list. 229 * 230 * @return A List of <code>CachedObject</code> objects held in the cache 231 */ 232 public List getCachedObjects() 233 { 234 ArrayList objects = new ArrayList(this.cache.size()); 235 synchronized (this) 236 { 237 for (Iterator itr = this.cache.keySet().iterator(); itr.hasNext();) 238 { 239 String key = (String) itr.next(); 240 CachedObject obj = null; 241 try 242 { 243 obj = getObject(key); 244 } 245 catch (ObjectExpiredException oee) 246 { 247 // this is OK we just do not want this object 248 continue; 249 } 250 objects.add(obj); 251 } 252 } 253 return objects; 254 } 255 256 /** 257 * Circle through the cache and remove stale objects. Frequency is 258 * determined by the cacheCheckFrequency property. 259 */ 260 public void run() 261 { 262 while (this.continueThread) 263 { 264 // Sleep for amount of time set in cacheCheckFrequency - 265 // default = 5 seconds. 266 try 267 { 268 Thread.sleep(this.cacheCheckFrequency); 269 } 270 catch (InterruptedException exc) 271 { 272 if (!this.continueThread) 273 { 274 return; 275 } 276 } 277 278 clearCache(); 279 } 280 } 281 282 /** 283 * Iterate through the cache and remove or refresh stale objects. 284 */ 285 public void clearCache() 286 { 287 List refreshThese = new ArrayList(20); 288 // Sync on this object so that other threads do not 289 // change the Hashtable while enumerating over it. 290 synchronized (this) 291 { 292 for (Enumeration e = this.cache.keys(); e.hasMoreElements();) 293 { 294 String key = (String) e.nextElement(); 295 CachedObject co = (CachedObject) this.cache.get(key); 296 if (co instanceof RefreshableCachedObject) 297 { 298 RefreshableCachedObject rco = (RefreshableCachedObject) co; 299 if (rco.isUntouched()) 300 { 301 this.cache.remove(key); 302 } 303 else if (rco.isStale()) 304 { 305 // to prolong holding the lock on this object 306 refreshThese.add(key); 307 } 308 } 309 else if (co.isStale()) 310 { 311 this.cache.remove(key); 312 } 313 } 314 } 315 for (Iterator i = refreshThese.iterator(); i.hasNext();) 316 { 317 String key = (String) i.next(); 318 CachedObject co = (CachedObject) this.cache.get(key); 319 RefreshableCachedObject rco = (RefreshableCachedObject) co; 320 rco.refresh(); 321 } 322 } 323 324 /** 325 * Returns the number of objects currently stored in the cache 326 * 327 * @return int number of object in the cache 328 */ 329 public int getNumberOfObjects() 330 { 331 return this.cache.size(); 332 } 333 334 /** 335 * Returns the current size of the cache. 336 * 337 * @return int representing current cache size in number of bytes 338 */ 339 public int getCacheSize() throws IOException 340 { 341 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 342 ObjectOutputStream out = new ObjectOutputStream(baos); 343 out.writeObject(this.cache); 344 out.flush(); 345 // 346 // Subtract 4 bytes from the length, because the serialization 347 // magic number (2 bytes) and version number (2 bytes) are 348 // both written to the stream before the object 349 // 350 int objectsize = baos.toByteArray().length - 4; 351 return objectsize; 352 } 353 354 /** 355 * Flush the cache of all objects. 356 */ 357 public void flushCache() 358 { 359 synchronized (this) 360 { 361 for (Enumeration e = this.cache.keys(); e.hasMoreElements();) 362 { 363 String key = (String) e.nextElement(); 364 this.cache.remove(key); 365 } 366 } 367 } 368 369 // ---------------- Avalon Lifecycle Methods --------------------- 370 /** 371 * Avalon component lifecycle method 372 */ 373 public void configure(Configuration conf) throws ConfigurationException 374 { 375 this.cacheCheckFrequency = conf.getAttributeAsLong( 376 CACHE_CHECK_FREQUENCY, DEFAULT_CACHE_CHECK_FREQUENCY); 377 this.cacheInitialSize = conf.getAttributeAsInteger(INITIAL_CACHE_SIZE, 378 DEFAULT_INITIAL_CACHE_SIZE); 379 } 380 381 /** 382 * Avalon component lifecycle method 383 */ 384 public void initialize() throws Exception 385 { 386 try 387 { 388 this.cache = new Hashtable(this.cacheInitialSize); 389 // Start housekeeping thread. 390 this.continueThread = true; 391 this.housekeeping = new Thread(this); 392 // Indicate that this is a system thread. JVM will quit only when 393 // there are no more active user threads. Settings threads spawned 394 // internally by Turbine as daemons allows commandline applications 395 // using Turbine to terminate in an orderly manner. 396 this.housekeeping.setDaemon(true); 397 this.housekeeping.start(); 398 } 399 catch (Exception e) 400 { 401 throw new Exception( 402 "DefaultGlobalCacheService failed to initialize", e); 403 } 404 } 405 406 /** 407 * Avalon component lifecycle method 408 */ 409 public void dispose() 410 { 411 this.continueThread = false; 412 this.housekeeping.interrupt(); 413 } 414 415 /** 416 * The name used to specify this component in TurbineResources.properties 417 * 418 * @deprecated part of the pre-avalon compatibility layer 419 */ 420 protected String getName() 421 { 422 return "GlobalCacheService"; 423 } 424 }