View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.common;
20  
21  import java.io.IOException;
22  import java.util.Collections;
23  import java.util.List;
24  
25  import org.mycore.services.mbeans.MCRJMXBridge;
26  
27  import com.google.common.cache.Cache;
28  import com.google.common.cache.CacheBuilder;
29  
30  /**
31   * Instances of this class can be used as object cache. Each MCRCache has a certain capacity, the maximum number of
32   * objects the cache will hold. When the cache is full and another object is put into the cache, the cache will discard
33   * the least recently used object to get place for the new object. The cache will always hold the most recently used
34   * objects by updating its internal structure whenever an object is get from the cache or put into the cache. The cache
35   * also provides methods for getting the current cache hit rate and fill rate. Like in a hashtable, an MCRCache uses a
36   * unique key for each object.
37   * 
38   * @see java.util.Hashtable
39   * @author Frank Lützenkirchen
40   * @version $Revision$ $Date$
41   */
42  public class MCRCache<K, V> {
43      /** Tch type string for the MCRCacheJMXBridge */
44      protected String type;
45  
46      Cache<K, MCRCacheEntry<V>> backingCache;
47  
48      private long capacity;
49  
50      /**
51       * Creates a new cache with a given capacity.
52       *
53       * @param capacity
54       *            the maximum number of objects this cache will hold
55       * @param type
56       *            the type string for MCRCacheJMXBridge
57       */
58      public MCRCache(long capacity, String type) {
59          backingCache = CacheBuilder.newBuilder().recordStats().maximumSize(capacity).build();
60          this.capacity = capacity;
61          this.type = type;
62          Object mbean = new MCRCacheManager(this);
63          MCRJMXBridge.register(mbean, "MCRCache", type);
64      }
65  
66      /**
67       * A small sample program for testing this class.
68       */
69      public static void main(String[] args) {
70          MCRCache<String, String> cache = new MCRCache<>(4, "Small Sample Program");
71          System.out.println(cache);
72          cache.put("a", "Anton");
73          cache.put("b", "Bohnen");
74          cache.put("c", "Cache");
75          System.out.println(cache);
76          cache.get("d");
77          cache.get("c");
78          cache.put("d", "Dieter");
79          cache.put("e", "Egon");
80          cache.put("f", "Frank");
81          cache.get("c");
82          System.out.println(cache);
83      }
84  
85      /**
86       * Puts an object into the cache, storing it under the given key. If the cache is already full, the least recently
87       * used object will be removed from the cache first. If the cache already contains an entry under the key provided,
88       * this entry is replaced.
89       *
90       * @param key
91       *            the non-null key to store the object under
92       * @param value
93       *            the non-null object to be put into the cache
94       */
95      public void put(K key, V value) {
96          if (key == null) {
97              throw new NullPointerException("The key of a cache entry may not be null.");
98          }
99          if (value == null) {
100             throw new NullPointerException("The value of a cache entry may not be null.");
101         }
102         MCRCacheEntry<V> entry = new MCRCacheEntry<>(value);
103         backingCache.put(key, entry);
104     }
105 
106     /**
107      * Puts an object into the cache, storing it under the given key. If the cache is already full, the least recently
108      * used object will be removed from the cache first. If the cache already contains an entry under the key provided,
109      * this entry is replaced.
110      *
111      * @param key
112      *            the non-null key to store the object under
113      * @param value
114      *            the non-null object to be put into the cache
115      * @param insertTime
116      *            the given last modified time for this key
117      */
118     public void put(K key, V value, long insertTime) {
119         if (key == null) {
120             throw new NullPointerException("The key of a cache entry may not be null.");
121         }
122         if (value == null) {
123             throw new NullPointerException("The value of a cache entry may not be null.");
124         }
125         MCRCacheEntry<V> entry = new MCRCacheEntry<>(value);
126         entry.insertTime = insertTime;
127         backingCache.put(key, entry);
128     }
129 
130     /**
131      * Removes an object from the cache for the given key.
132      *
133      * @param key
134      *            the key for the object you want to remove from this cache
135      */
136     public void remove(K key) {
137         if (key == null) {
138             throw new MCRUsageException("The value of the argument key is null.");
139         }
140         backingCache.invalidate(key);
141     }
142 
143     /**
144      * Returns an object from the cache for the given key, or null if there currently is no object in the cache with
145      * this key.
146      *
147      * @param key
148      *            the key for the object you want to get from this cache
149      * @return the cached object, or null
150      */
151     public V get(K key) {
152         MCRCacheEntry<V> found = backingCache.getIfPresent(key);
153         return found == null ? null : found.value;
154     }
155 
156     /**
157      * Returns an object from the cache for the given key, but only if the cache entry is not older than the given
158      * timestamp. If there currently is no object in the cache with this key, null is returned. If the cache entry is
159      * older than the timestamp, the entry is removed from the cache and null is returned.
160      *
161      * @param key
162      *            the key for the object you want to get from this cache
163      * @param time
164      *            the timestamp to check that the cache entry is up to date
165      * @return the cached object, or null
166      */
167     public V getIfUpToDate(K key, long time) {
168         MCRCacheEntry<V> found = backingCache.getIfPresent(key);
169 
170         if (found == null || found.insertTime < time) {
171             return null;
172         }
173 
174         if (found.insertTime >= time) {
175             found.lookUpTime = System.currentTimeMillis();
176             return found.value;
177         }
178         backingCache.invalidate(key);
179         return null;
180     }
181 
182     /**
183      * Returns an object from the cache for the given key, but only if the cache entry is not older than the given
184      * timestamp of the {@link ModifiedHandle}. In contrast to {@link #getIfUpToDate(Object, long)} you can submit your
185      * own handle that returns the last modified timestamp after a certain period is over. Use this method if
186      * determining lastModified date is rather expensive and cache access is often.
187      *
188      * @param key
189      *            the key for the object you want to get from this cache
190      * @param handle
191      *            the timestamp to check that the cache entry is up to date
192      * @return the cached object, or null
193      * @throws IOException
194      *             thrown by {@link ModifiedHandle#getLastModified()}
195      * @since 2.1.81
196      */
197     public V getIfUpToDate(K key, ModifiedHandle handle) throws IOException {
198         MCRCacheEntry<V> found = backingCache.getIfPresent(key);
199         if (found == null) {
200             return null;
201         }
202         if (System.currentTimeMillis() - found.lookUpTime > handle.getCheckPeriod()) {
203             if (found.insertTime >= handle.getLastModified()) {
204                 found.lookUpTime = System.currentTimeMillis();
205                 return found.value;
206             }
207             backingCache.invalidate(key);
208             return null;
209         } else {
210             return found.value;
211         }
212     }
213 
214     /**
215      * Returns the number of objects currently cached.
216      *
217      * @return the number of objects currently cached
218      */
219     public long getCurrentSize() {
220         backingCache.cleanUp();
221         return backingCache.size();
222     }
223 
224     /**
225      * Returns the capacity of this cache. This is the maximum number of objects this cache will hold at a time.
226      *
227      * @return the capacity of this cache
228      */
229     public long getCapacity() {
230         return capacity;
231     }
232 
233     /**
234      * Changes the capacity of this cache. This is the maximum number of objects that will be cached at a time. If the
235      * new capacity is smaller than the current number of objects in the cache, the least recently used objects will be
236      * removed from the cache.
237      *
238      * @param capacity
239      *            the maximum number of objects this cache will hold
240      */
241     public synchronized void setCapacity(long capacity) {
242         this.capacity = capacity;
243         Cache<K, MCRCacheEntry<V>> newCache = CacheBuilder.newBuilder().recordStats().maximumSize(capacity).build();
244         newCache.putAll(backingCache.asMap());
245         Cache<K, MCRCacheEntry<V>> oldCache = backingCache;
246         backingCache = newCache;
247         oldCache.invalidateAll();
248     }
249 
250     /**
251      * Returns true if this cache is full.
252      *
253      * @return true if this cache is full
254      */
255     public boolean isFull() {
256         backingCache.cleanUp();
257         return backingCache.size() == capacity;
258     }
259 
260     /**
261      * Returns true if this cache is empty.
262      *
263      * @return true if this cache is empty
264      */
265     public boolean isEmpty() {
266         backingCache.cleanUp();
267         return backingCache.size() == 0;
268     }
269 
270     /**
271      * Returns the fill rate of this cache. This is the current number of objects in the cache diveded by its capacity.
272      *
273      * @return the fill rate of this cache as double value
274      */
275     public double getFillRate() {
276         return capacity == 0 ? 1.0 : (double) getCurrentSize() / (double) capacity;
277     }
278 
279     /**
280      * Returns the hit rate of this cache. This is the number of successful hits divided by the total number of get
281      * requests so far. Using this ratio can help finding the appropriate cache capacity.
282      *
283      * @return the hit rate of this cache as double value
284      */
285     public double getHitRate() {
286         return backingCache.stats().hitRate();
287     }
288 
289     /**
290      * Clears the cache by removing all entries from the cache
291      */
292     public void clear() {
293         backingCache.invalidateAll();
294     }
295 
296     /**
297      * Returns a String containing information about cache capacity, size, current fill rate and hit rate. Useful for
298      * testing and debugging.
299      */
300     @Override
301     public String toString() {
302 
303         return "Cache capacity:  " + capacity + "\n" + "Cache size:      " + backingCache.size() + "\n"
304             + "Cache fill rate: " + getFillRate() + "\n" + "Cache hit rate:  " + getHitRate();
305     }
306 
307     public void close() {
308         MCRJMXBridge.unregister("MCRCache", type);
309         clear();
310     }
311 
312     /**
313      * Returns an iterable list of keys to the cached objects.
314      */
315     public List<K> keys() {
316         return Collections.list(Collections.enumeration(backingCache.asMap().keySet()));
317     }
318 
319     /**
320      * @author Thomas Scheffler (yagee)
321      */
322     public interface ModifiedHandle {
323 
324         /**
325          * check distance in ms. After this period of time use {@link #getLastModified()} to check if object is still
326          * up-to-date.
327          */
328         long getCheckPeriod();
329 
330         /**
331          * returns timestamp when the cache value was last modified.
332          */
333         long getLastModified() throws IOException;
334 
335     }
336 
337     private static class MCRCacheEntry<V> {
338         public long lookUpTime;
339 
340         V value;
341 
342         long insertTime;
343 
344         MCRCacheEntry(V value) {
345             this.value = value;
346             this.insertTime = System.currentTimeMillis();
347         }
348     }
349 }