001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.cache; 029 030import org.opencms.main.CmsLog; 031 032import org.apache.commons.logging.Log; 033 034/** 035 * Implements an LRU (last recently used) cache.<p> 036 * 037 * The idea of this cache is to separate the caching policy from the data structure 038 * where the cached objects are stored. The advantage of doing so is, that the CmsFlexLruCache 039 * can identify the last-recently-used object in O(1), whereas you would need at least 040 * O(n) to traverse the data structure that stores the cached objects. Second, you can 041 * easily use the CmsFlexLruCache to get an LRU cache, no matter what data structure is used to 042 * store your objects. 043 * <p> 044 * The cache policy is affected by the "costs" of the objects being cached. Valuable cache costs 045 * might be the byte size of the cached objects for example. 046 * <p> 047 * To add/remove cached objects from the data structure that stores them, the objects have to 048 * implement the methods defined in the interface I_CmsLruCacheObject to be notified when they 049 * are added/removed from the CmsFlexLruCache.<p> 050 * 051 * @see org.opencms.cache.I_CmsLruCacheObject 052 * 053 * @since 6.0.0 054 */ 055public class CmsLruCache extends java.lang.Object { 056 057 /** The log object for this class. */ 058 private static final Log LOG = CmsLog.getLog(CmsLruCache.class); 059 060 /** The average sum of costs the cached objects. */ 061 private long m_avgCacheCosts; 062 063 /** The head of the list of double linked LRU cache objects. */ 064 private I_CmsLruCacheObject m_listHead; 065 066 /** The tail of the list of double linked LRU cache objects. */ 067 private I_CmsLruCacheObject m_listTail; 068 069 /** The maximum sum of costs the cached objects might reach. */ 070 private long m_maxCacheCosts; 071 072 /** The maximum costs of cacheable objects. */ 073 private int m_maxObjectCosts; 074 075 /** The costs of all cached objects. */ 076 private int m_objectCosts; 077 078 /** The sum of all cached objects. */ 079 private int m_objectCount; 080 081 /** 082 * The constructor with all options.<p> 083 * 084 * @param theMaxCacheCosts the maximum cache costs of all cached objects 085 * @param theAvgCacheCosts the average cache costs of all cached objects 086 * @param theMaxObjectCosts the maximum allowed cache costs per object. Set theMaxObjectCosts to -1 if you don't want to limit the max. allowed cache costs per object 087 */ 088 public CmsLruCache(long theMaxCacheCosts, long theAvgCacheCosts, int theMaxObjectCosts) { 089 090 m_maxCacheCosts = theMaxCacheCosts; 091 m_avgCacheCosts = theAvgCacheCosts; 092 m_maxObjectCosts = theMaxObjectCosts; 093 } 094 095 /** 096 * Adds a new object to this cache.<p> 097 * 098 * If add the same object more than once, 099 * the object is touched instead.<p> 100 * 101 * @param theCacheObject the object being added to the cache 102 * @return true if the object was added to the cache, false if the object was denied because its cache costs were higher than the allowed max. cache costs per object 103 */ 104 public synchronized boolean add(I_CmsLruCacheObject theCacheObject) { 105 106 if (theCacheObject == null) { 107 // null can't be added or touched in the cache 108 return false; 109 } 110 111 // only objects with cache costs < the max. allowed object cache costs can be cached! 112 if ((m_maxObjectCosts != -1) && (theCacheObject.getLruCacheCosts() > m_maxObjectCosts)) { 113 if (LOG.isInfoEnabled()) { 114 LOG.info( 115 Messages.get().getBundle().key( 116 Messages.LOG_CACHE_COSTS_TOO_HIGH_2, 117 new Integer(theCacheObject.getLruCacheCosts()), 118 new Integer(m_maxObjectCosts))); 119 } 120 return false; 121 } 122 123 if (!isCached(theCacheObject)) { 124 // add the object to the list of all cached objects in the cache 125 addHead(theCacheObject); 126 } else { 127 touch(theCacheObject); 128 } 129 130 // check if the cache has to trash the last-recently-used objects before adding a new object 131 if (m_objectCosts > m_maxCacheCosts) { 132 gc(); 133 } 134 135 return true; 136 } 137 138 /** 139 * Removes all cached objects in this cache.<p> 140 */ 141 public synchronized void clear() { 142 143 // remove all objects from the linked list from the tail to the head: 144 I_CmsLruCacheObject currentObject = m_listTail; 145 while (currentObject != null) { 146 currentObject = currentObject.getNextLruObject(); 147 removeTail(); 148 } 149 150 // reset the data structure 151 m_objectCosts = 0; 152 m_objectCount = 0; 153 m_listHead = null; 154 m_listTail = null; 155 } 156 157 /** 158 * Returns the average costs of all cached objects.<p> 159 * 160 * @return the average costs of all cached objects 161 */ 162 public long getAvgCacheCosts() { 163 164 return m_avgCacheCosts; 165 } 166 167 /** 168 * Returns the max costs of all cached objects.<p> 169 * 170 * @return the max costs of all cached objects 171 */ 172 public long getMaxCacheCosts() { 173 174 return m_maxCacheCosts; 175 } 176 177 /** 178 * Returns the max allowed costs per cached object.<p> 179 * 180 * @return the max allowed costs per cached object 181 */ 182 public int getMaxObjectCosts() { 183 184 return m_maxObjectCosts; 185 } 186 187 /** 188 * Returns the current costs of all cached objects.<p> 189 * 190 * @return the current costs of all cached objects 191 */ 192 public int getObjectCosts() { 193 194 return m_objectCosts; 195 } 196 197 /** 198 * Removes an object from the list of all cached objects in this cache, 199 * no matter what position it has inside the list.<p> 200 * 201 * @param theCacheObject the object being removed from the list of all cached objects 202 * @return a reference to the object that was removed 203 */ 204 public synchronized I_CmsLruCacheObject remove(I_CmsLruCacheObject theCacheObject) { 205 206 if (!isCached(theCacheObject)) { 207 // theCacheObject is null or not inside the cache 208 return null; 209 } 210 211 // set the list pointers correct 212 if (theCacheObject.getNextLruObject() == null) { 213 // remove the object from the head pos. 214 I_CmsLruCacheObject newHead = theCacheObject.getPreviousLruObject(); 215 216 if (newHead != null) { 217 // if newHead is null, theCacheObject 218 // was the only object in the cache 219 newHead.setNextLruObject(null); 220 } 221 222 m_listHead = newHead; 223 } else if (theCacheObject.getPreviousLruObject() == null) { 224 // remove the object from the tail pos. 225 I_CmsLruCacheObject newTail = theCacheObject.getNextLruObject(); 226 227 if (newTail != null) { 228 // if newTail is null, theCacheObject 229 // was the only object in the cache 230 newTail.setPreviousLruObject(null); 231 } 232 233 m_listTail = newTail; 234 } else { 235 // remove the object from within the list 236 theCacheObject.getPreviousLruObject().setNextLruObject(theCacheObject.getNextLruObject()); 237 theCacheObject.getNextLruObject().setPreviousLruObject(theCacheObject.getPreviousLruObject()); 238 } 239 240 // update cache stats. and notify the cached object 241 decreaseCache(theCacheObject); 242 243 return theCacheObject; 244 } 245 246 /** 247 * Returns the count of all cached objects.<p> 248 * 249 * @return the count of all cached objects 250 */ 251 public int size() { 252 253 return m_objectCount; 254 } 255 256 /** 257 * Returns a string representing the current state of the cache.<p> 258 * 259 * @return a string representing the current state of the cache 260 */ 261 @Override 262 public String toString() { 263 264 StringBuffer buf = new StringBuffer(); 265 buf.append("max. costs: " + m_maxCacheCosts).append(", "); 266 buf.append("avg. costs: " + m_avgCacheCosts).append(", "); 267 buf.append("max. costs/object: " + m_maxObjectCosts).append(", "); 268 buf.append("costs: " + m_objectCosts).append(", "); 269 buf.append("count: " + m_objectCount); 270 return buf.toString(); 271 } 272 273 /** 274 * Touch an existing object in this cache, in the sense that it's "last-recently-used" state 275 * is updated.<p> 276 * 277 * @param theCacheObject the object being touched 278 * @return true if an object was found and touched 279 */ 280 public synchronized boolean touch(I_CmsLruCacheObject theCacheObject) { 281 282 if (!isCached(theCacheObject)) { 283 return false; 284 } 285 286 // only objects with cache costs < the max. allowed object cache costs can be cached! 287 if ((m_maxObjectCosts != -1) && (theCacheObject.getLruCacheCosts() > m_maxObjectCosts)) { 288 if (LOG.isInfoEnabled()) { 289 LOG.info( 290 Messages.get().getBundle().key( 291 Messages.LOG_CACHE_COSTS_TOO_HIGH_2, 292 new Integer(theCacheObject.getLruCacheCosts()), 293 new Integer(m_maxObjectCosts))); 294 } 295 remove(theCacheObject); 296 return false; 297 } 298 299 // set the list pointers correct 300 I_CmsLruCacheObject nextObj = theCacheObject.getNextLruObject(); 301 if (nextObj == null) { 302 // case 1: the object is already at the head pos. 303 return true; 304 } 305 I_CmsLruCacheObject prevObj = theCacheObject.getPreviousLruObject(); 306 if (prevObj == null) { 307 // case 2: the object at the tail pos., remove it from the tail to put it to the front as the new head 308 I_CmsLruCacheObject newTail = nextObj; 309 newTail.setPreviousLruObject(null); 310 m_listTail = newTail; 311 } else { 312 // case 3: the object is somewhere within the list, remove it to put it the front as the new head 313 prevObj.setNextLruObject(nextObj); 314 nextObj.setPreviousLruObject(prevObj); 315 } 316 317 // set the touched object as the new head in the linked list: 318 I_CmsLruCacheObject oldHead = m_listHead; 319 if (oldHead != null) { 320 oldHead.setNextLruObject(theCacheObject); 321 theCacheObject.setNextLruObject(null); 322 theCacheObject.setPreviousLruObject(oldHead); 323 } 324 m_listHead = theCacheObject; 325 326 return true; 327 } 328 329 /** 330 * Adds a cache object as the new haed to the list of all cached objects in this cache.<p> 331 * 332 * @param theCacheObject the object being added as the new head to the list of all cached objects 333 */ 334 private void addHead(I_CmsLruCacheObject theCacheObject) { 335 336 // set the list pointers correct 337 if (m_objectCount > 0) { 338 // there is at least 1 object already in the list 339 I_CmsLruCacheObject oldHead = m_listHead; 340 oldHead.setNextLruObject(theCacheObject); 341 theCacheObject.setPreviousLruObject(oldHead); 342 m_listHead = theCacheObject; 343 } else { 344 // it is the first object to be added to the list 345 m_listTail = theCacheObject; 346 m_listHead = theCacheObject; 347 theCacheObject.setPreviousLruObject(null); 348 } 349 theCacheObject.setNextLruObject(null); 350 351 // update cache stats. and notify the cached object 352 increaseCache(theCacheObject); 353 } 354 355 /** 356 * Decrease this caches statistics 357 * and notify the cached object that it was removed from this cache.<p> 358 * 359 * @param theCacheObject the object being notified that it was removed from the cache 360 */ 361 private void decreaseCache(I_CmsLruCacheObject theCacheObject) { 362 363 // notify the object that it was now removed from the cache 364 //theCacheObject.notify(); 365 theCacheObject.removeFromLruCache(); 366 367 // set the list pointers to null 368 theCacheObject.setNextLruObject(null); 369 theCacheObject.setPreviousLruObject(null); 370 371 // update the cache stats. 372 m_objectCosts -= theCacheObject.getLruCacheCosts(); 373 m_objectCount--; 374 } 375 376 /** 377 * Removes the last recently used objects from the list of all cached objects as long 378 * as the costs of all cached objects are higher than the allowed avg. costs of the cache.<p> 379 */ 380 private void gc() { 381 382 I_CmsLruCacheObject currentObject = m_listTail; 383 while (currentObject != null) { 384 if (m_objectCosts < m_avgCacheCosts) { 385 break; 386 } 387 currentObject = currentObject.getNextLruObject(); 388 removeTail(); 389 } 390 } 391 392 /** 393 * Increase this caches statistics 394 * and notify the cached object that it was added to this cache.<p> 395 * 396 * @param theCacheObject the object being notified that it was added to the cache 397 */ 398 private void increaseCache(I_CmsLruCacheObject theCacheObject) { 399 400 // notify the object that it was now added to the cache 401 //theCacheObject.notify(); 402 theCacheObject.addToLruCache(); 403 404 // update the cache stats. 405 m_objectCosts += theCacheObject.getLruCacheCosts(); 406 m_objectCount++; 407 } 408 409 /** 410 * Test if a given object resides inside the cache.<p> 411 * 412 * @param theCacheObject the object to test 413 * @return true if the object is inside the cache, false otherwise 414 */ 415 private boolean isCached(I_CmsLruCacheObject theCacheObject) { 416 417 if ((theCacheObject == null) || (m_objectCount == 0)) { 418 // the cache is empty or the object is null (which is never cached) 419 return false; 420 } 421 422 I_CmsLruCacheObject nextObj = theCacheObject.getNextLruObject(); 423 I_CmsLruCacheObject prevObj = theCacheObject.getPreviousLruObject(); 424 425 if ((nextObj != null) || (prevObj != null)) { 426 // the object has either a predecessor or successor in the linked 427 // list of all cached objects, so it is inside the cache 428 return true; 429 } 430 431 // both nextObj and preObj are null 432 if ((m_objectCount == 1) 433 && (m_listHead != null) 434 && (m_listTail != null) 435 && m_listHead.equals(theCacheObject) 436 && m_listTail.equals(theCacheObject)) { 437 // the object is the one and only object in the cache 438 return true; 439 } 440 441 return false; 442 } 443 444 /** 445 * Removes the tailing object from the list of all cached objects.<p> 446 */ 447 private synchronized void removeTail() { 448 449 I_CmsLruCacheObject oldTail = m_listTail; 450 if (oldTail != null) { 451 I_CmsLruCacheObject newTail = oldTail.getNextLruObject(); 452 453 // set the list pointers correct 454 if (newTail != null) { 455 // there are still objects remaining in the list 456 newTail.setPreviousLruObject(null); 457 m_listTail = newTail; 458 } else { 459 // we removed the last object from the list 460 m_listTail = null; 461 m_listHead = null; 462 } 463 464 // update cache stats. and notify the cached object 465 decreaseCache(oldTail); 466 } 467 } 468}