001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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 & Co. KG, 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                        Integer.valueOf(theCacheObject.getLruCacheCosts()),
118                        Integer.valueOf(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        boolean nextNull = (theCacheObject.getNextLruObject() == null);
213        boolean prevNull = (theCacheObject.getPreviousLruObject() == null);
214        if (prevNull && nextNull) {
215            m_listHead = null;
216            m_listTail = null;
217        } else if (nextNull) {
218            // remove the object from the head pos.
219            I_CmsLruCacheObject newHead = theCacheObject.getPreviousLruObject();
220            newHead.setNextLruObject(null);
221            m_listHead = newHead;
222        } else if (prevNull) {
223            // remove the object from the tail pos.
224            I_CmsLruCacheObject newTail = theCacheObject.getNextLruObject();
225            newTail.setPreviousLruObject(null);
226            m_listTail = newTail;
227        } else {
228            // remove the object from within the list
229            theCacheObject.getPreviousLruObject().setNextLruObject(theCacheObject.getNextLruObject());
230            theCacheObject.getNextLruObject().setPreviousLruObject(theCacheObject.getPreviousLruObject());
231        }
232
233        // update cache stats. and notify the cached object
234        decreaseCache(theCacheObject);
235        return theCacheObject;
236    }
237
238    /**
239     * Returns the count of all cached objects.<p>
240     *
241     * @return the count of all cached objects
242     */
243    public int size() {
244
245        return m_objectCount;
246    }
247
248    /**
249     * Returns a string representing the current state of the cache.<p>
250     *
251     * @return a string representing the current state of the cache
252     */
253    @Override
254    public String toString() {
255
256        StringBuffer buf = new StringBuffer();
257        buf.append("max. costs: " + m_maxCacheCosts).append(", ");
258        buf.append("avg. costs: " + m_avgCacheCosts).append(", ");
259        buf.append("max. costs/object: " + m_maxObjectCosts).append(", ");
260        buf.append("costs: " + m_objectCosts).append(", ");
261        buf.append("count: " + m_objectCount);
262        return buf.toString();
263    }
264
265    /**
266     * Touch an existing object in this cache, in the sense that it's "last-recently-used" state
267     * is updated.<p>
268     *
269     * @param theCacheObject the object being touched
270     * @return true if an object was found and touched
271     */
272    public synchronized boolean touch(I_CmsLruCacheObject theCacheObject) {
273
274        if (!isCached(theCacheObject)) {
275            return false;
276        }
277
278        // only objects with cache costs < the max. allowed object cache costs can be cached!
279        if ((m_maxObjectCosts != -1) && (theCacheObject.getLruCacheCosts() > m_maxObjectCosts)) {
280            if (LOG.isInfoEnabled()) {
281                LOG.info(
282                    Messages.get().getBundle().key(
283                        Messages.LOG_CACHE_COSTS_TOO_HIGH_2,
284                        Integer.valueOf(theCacheObject.getLruCacheCosts()),
285                        Integer.valueOf(m_maxObjectCosts)));
286            }
287            remove(theCacheObject);
288            return false;
289        }
290
291        // set the list pointers correct
292        I_CmsLruCacheObject nextObj = theCacheObject.getNextLruObject();
293        if (nextObj == null) {
294            // case 1: the object is already at the head pos.
295            return true;
296        }
297        I_CmsLruCacheObject prevObj = theCacheObject.getPreviousLruObject();
298        if (prevObj == null) {
299            // case 2: the object at the tail pos., remove it from the tail to put it to the front as the new head
300            I_CmsLruCacheObject newTail = nextObj;
301            newTail.setPreviousLruObject(null);
302            m_listTail = newTail;
303        } else {
304            // case 3: the object is somewhere within the list, remove it to put it the front as the new head
305            prevObj.setNextLruObject(nextObj);
306            nextObj.setPreviousLruObject(prevObj);
307        }
308        // set the touched object as the new head in the linked list:
309        I_CmsLruCacheObject oldHead = m_listHead;
310        if (oldHead != null) {
311            oldHead.setNextLruObject(theCacheObject);
312            theCacheObject.setNextLruObject(null);
313            theCacheObject.setPreviousLruObject(oldHead);
314        }
315        m_listHead = theCacheObject;
316
317        return true;
318    }
319
320    /**
321     * Adds a cache object as the new haed to the list of all cached objects in this cache.<p>
322     *
323     * @param theCacheObject the object being added as the new head to the list of all cached objects
324     */
325    private void addHead(I_CmsLruCacheObject theCacheObject) {
326
327        // set the list pointers correct
328        if (m_objectCount > 0) {
329            // there is at least 1 object already in the list
330            I_CmsLruCacheObject oldHead = m_listHead;
331            oldHead.setNextLruObject(theCacheObject);
332            theCacheObject.setPreviousLruObject(oldHead);
333            m_listHead = theCacheObject;
334        } else {
335            // it is the first object to be added to the list
336            m_listTail = theCacheObject;
337            m_listHead = theCacheObject;
338            theCacheObject.setPreviousLruObject(null);
339        }
340        theCacheObject.setNextLruObject(null);
341
342        // update cache stats. and notify the cached object
343        increaseCache(theCacheObject);
344    }
345
346    /**
347     * Decrease this caches statistics
348     * and notify the cached object that it was removed from this cache.<p>
349     *
350     * @param theCacheObject the object being notified that it was removed from the cache
351     */
352    private void decreaseCache(I_CmsLruCacheObject theCacheObject) {
353
354        // notify the object that it was now removed from the cache
355        //theCacheObject.notify();
356        theCacheObject.removeFromLruCache();
357
358        // set the list pointers to null
359        theCacheObject.setNextLruObject(null);
360        theCacheObject.setPreviousLruObject(null);
361
362        // update the cache stats.
363        m_objectCosts -= theCacheObject.getLruCacheCosts();
364        m_objectCount--;
365    }
366
367    /**
368     * Removes the last recently used objects from the list of all cached objects as long
369     * as the costs of all cached objects are higher than the allowed avg. costs of the cache.<p>
370     */
371    private void gc() {
372
373        I_CmsLruCacheObject currentObject = m_listTail;
374        while (currentObject != null) {
375            if (m_objectCosts < m_avgCacheCosts) {
376                break;
377            }
378            currentObject = currentObject.getNextLruObject();
379            removeTail();
380        }
381    }
382
383    /**
384     * Increase this caches statistics
385     * and notify the cached object that it was added to this cache.<p>
386     *
387     * @param theCacheObject the object being notified that it was added to the cache
388     */
389    private void increaseCache(I_CmsLruCacheObject theCacheObject) {
390
391        // notify the object that it was now added to the cache
392        //theCacheObject.notify();
393        theCacheObject.addToLruCache();
394
395        // update the cache stats.
396        m_objectCosts += theCacheObject.getLruCacheCosts();
397        m_objectCount++;
398    }
399
400    /**
401     * Test if a given object resides inside the cache.<p>
402     *
403     * @param theCacheObject the object to test
404     * @return true if the object is inside the cache, false otherwise
405     */
406    private boolean isCached(I_CmsLruCacheObject theCacheObject) {
407
408        if ((theCacheObject == null) || (m_objectCount == 0)) {
409            // the cache is empty or the object is null (which is never cached)
410            return false;
411        }
412
413        I_CmsLruCacheObject nextObj = theCacheObject.getNextLruObject();
414        I_CmsLruCacheObject prevObj = theCacheObject.getPreviousLruObject();
415
416        if ((nextObj != null) || (prevObj != null)) {
417            // the object has either a predecessor or successor in the linked
418            // list of all cached objects, so it is inside the cache
419            return true;
420        }
421
422        // both nextObj and preObj are null
423        if ((m_objectCount == 1)
424            && (m_listHead != null)
425            && (m_listTail != null)
426            && m_listHead.equals(theCacheObject)
427            && m_listTail.equals(theCacheObject)) {
428            // the object is the one and only object in the cache
429            return true;
430        }
431
432        return false;
433    }
434
435    /**
436     * Removes the tailing object from the list of all cached objects.<p>
437     */
438    private synchronized void removeTail() {
439
440        I_CmsLruCacheObject oldTail = m_listTail;
441        if (oldTail != null) {
442            I_CmsLruCacheObject newTail = oldTail.getNextLruObject();
443
444            // set the list pointers correct
445            if (newTail != null) {
446                // there are still objects remaining in the list
447                newTail.setPreviousLruObject(null);
448                m_listTail = newTail;
449            } else {
450                // we removed the last object from the list
451                m_listTail = null;
452                m_listHead = null;
453            }
454
455            // update cache stats. and notify the cached object
456            decreaseCache(oldTail);
457        }
458    }
459}