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}