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 }