001    /*
002     @license.text@ 
003     */
004    package biz.hammurapi.cache;
005    
006    import java.lang.ref.ReferenceQueue;
007    import java.lang.ref.SoftReference;
008    import java.util.HashMap;
009    import java.util.HashSet;
010    import java.util.Iterator;
011    import java.util.LinkedList;
012    import java.util.Map;
013    import java.util.Set;
014    import java.util.Timer;
015    import java.util.TimerTask;
016    import java.util.WeakHashMap;
017    
018    import biz.hammurapi.config.Component;
019    import biz.hammurapi.config.ConfigurationException;
020    import biz.hammurapi.metrics.MeasurementCategory;
021    import biz.hammurapi.util.Acceptor;
022    
023    
024    /**
025     * Memory sensitive cache. Uses soft references to cache objects.
026     * @author Pavel Vlasov
027     * @version $Revision: 1.7 $
028     */
029    public class MemoryCache extends AbstractProducer implements Cache, Component {
030            protected Map cache=new HashMap();
031            protected Map reverseCache=new WeakHashMap();
032            
033            private ReferenceQueue queue=new ReferenceQueue();
034            protected Producer producer;
035            private Cache fallBack;
036            private MeasurementCategory measurementCategory;
037                    
038            private void addMeasurement(String name, double value, long time) {
039                    if (measurementCategory!=null) {
040                            measurementCategory.addMeasurement(name, value, time);
041                    }
042            }
043            
044            private void addMeasurement(String name, double value) {
045                    if (measurementCategory!=null) {
046                            measurementCategory.addMeasurement(name, value, 0);
047                    }
048            }
049            
050            private class CacheReference extends SoftReference {
051    
052                    private Object key;
053                    private long expires;
054                    private long time;
055    
056                    /**
057                     * @param referent
058                     * @param q
059                     * @param expires
060                     */
061                    public CacheReference(Object key, Object value, long time, long expires) {
062                            super(value, queue);
063                            this.key=key;
064                            this.expires=expires;
065                            this.time=time;
066                    }
067    
068                    /**
069                     * @param string
070                     * @param i
071                     * @param l
072                     */
073                    public void addMeasurement(String name, double value) {
074                            MemoryCache.this.addMeasurement(name, value);
075                    }               
076            }
077            
078            /**
079             * Used as entry if fallBack cache is used.
080             * @author Pavel Vlasov
081             * @version $Revision: 1.7 $
082             */
083            private class FallBackEntry {
084                    Object value;
085                    private long expires;
086                    private long time;
087                    private Object key;
088                    
089                    FallBackEntry(Object key, Object value, long time, long expires) {
090                            this.key=key;
091                            this.value=value;
092                            this.expires=expires;
093                            this.time=time;
094                    }
095                    
096                    protected void finalize() throws Throwable {
097                            if (fallBack.isActive() && (expires<=0 || System.currentTimeMillis()<expires)) {
098                                    fallBack.put(key, value, time, expires);
099                            }
100                            
101                            super.finalize();
102                    }               
103            }
104            
105            private static class ReferenceThread extends Thread {
106                    private ReferenceQueue queue;
107                    private Map cache;
108    
109                    private ReferenceThread(ReferenceQueue queue, Map cache) {
110                            this.queue=queue;
111                            this.cache=cache;
112                            setDaemon(true);
113                            setName("Removes cleared entries");
114                            start();
115                    }
116                    
117                    public void run() {
118                            try {
119                                    while (true) {
120                                            CacheReference cacheReference = (CacheReference) queue.remove();
121                                            Object key=cacheReference.key;
122                                            synchronized (cache) {
123                                                    cache.remove(key);                                              
124                                            }
125                                            cacheReference.addMeasurement("garbage collected", 1);
126                                    }
127                            } catch (InterruptedException e) {
128                                    return;
129                            }
130                    }
131            }
132            
133            private Thread referenceThread=new ReferenceThread(queue, cache);
134            
135            private static class JanitorTask extends TimerTask {
136                    private Map cache;
137                    private MeasurementCategory measurementCategory;
138                    private Map reverseCache;
139                    
140                    private void addMeasurement(String name, double value, long time) {
141                            if (measurementCategory!=null) {
142                                    measurementCategory.addMeasurement(name, value, time);
143                            }
144                    }
145                    
146                    private JanitorTask(Map cache, Map reverseCache, MeasurementCategory measurementCategory) {
147                            this.cache=cache;
148                            this.reverseCache=reverseCache;
149                            this.measurementCategory=measurementCategory;
150                    }
151                            
152                    public void run() {
153                            long now=System.currentTimeMillis();
154                            int expired=0;
155                            synchronized (cache) {
156                                    Iterator it=new LinkedList(cache.keySet()).iterator();
157                                    while (it.hasNext()) {
158                                            Object key=it.next();
159                                            CacheReference cacheReference = (CacheReference) cache.get(key);
160                                            long expires = (cacheReference).expires;
161                                            if (expires>0 && expires<now) {
162                                                    cache.remove(key);                                                              
163                                                    reverseCache.remove(cacheReference.get());
164                                                    expired++;
165                                            }
166                                    }
167                                    addMeasurement("size", cache.size(), now);
168                            }
169                            
170                            if (expired>0) {
171                                    addMeasurement("expired", expired, now);
172                            }
173                    }                       
174            }
175            
176            private TimerTask janitorTask;
177            
178            /**
179             * Default cache cleanup interval.
180             */
181            public static final long CLEANUP_INTERVAL=60000;
182    
183            /**
184             * Constructs cache with default cleanup interval (1 minute).
185             * @param producer Producer
186             * @param fallBack Fallback cache
187             * @param measurementCategory Measurement category to report cache statistics
188             */
189            public MemoryCache(Producer producer, Cache fallBack, MeasurementCategory measurementCategory) {
190                    this(producer, fallBack, measurementCategory, null, CLEANUP_INTERVAL);
191            }
192            
193            /**
194             * Constructs cache with default cleanup interval (1 minute).
195             * @param producer Producer
196             * @param fallBack Fallback cache
197             * @param measurementCategory Measurement category to report cache statistics
198             * @param cleanupInterval Interval between removals of expired entries.
199             * @param timer Timer which will invoke cleanup tasks, if it is null then a new timer is created internally. 
200             * Use this parameter to share timers between multiple caches in order to reduce number of threads in the 
201             * application.
202             */
203            public MemoryCache(Producer producer, Cache fallBack, MeasurementCategory measurementCategory, Timer timer, long cleanupInterval) {
204                    super();
205                    
206                    this.producer=producer;
207                    if (producer!=null) {
208                            producer.addCache(this);
209                    }
210                    
211                    this.fallBack=fallBack;
212                    if (fallBack!=null) {
213                            addCache(fallBack);
214                    }
215                    
216                    this.measurementCategory=measurementCategory;           
217                    this.timer=timer;
218                    
219                    this.cleanupInterval=cleanupInterval;
220            }
221            
222            private boolean isOwnTimer;
223            private Timer timer;
224            private long cleanupInterval;
225            
226            public void put(Object key, Object value, long time, long expirationTime) {
227                    checkShutdown();
228                    synchronized (cache) {
229                            cache.put(key, new CacheReference(key, toValue(key, value, time, expirationTime), time, expirationTime));
230                            reverseCache.put(value, key);
231                            addMeasurement("put", 1, time);
232                    }
233            }
234    
235            /**
236             * @param key
237             * @param value
238             * @param expirationTime
239             * @return
240             */
241            private Object toValue(Object key, Object value, long time, long expirationTime) {
242                    return fallBack==null ? value : new FallBackEntry(key, value, time, expirationTime);
243            }
244            
245            private Object fromValue(Object object) {
246                    if (object instanceof FallBackEntry) {
247                            return ((FallBackEntry) object).value;
248                    }
249                    return object;
250            }
251    
252            public Entry get(Object key) {
253                    checkShutdown();
254                    synchronized (cache) {
255                            CacheReference cr=(CacheReference) cache.get(key);
256                            long now = System.currentTimeMillis();
257                            addMeasurement("get", 1, now);
258                            boolean doFallBack=true;
259                            if (cr!=null) {
260                                    if ((cr.expires>0 && cr.expires<now) || cr.get()==null) {
261                                            cache.remove(key);      
262                                            reverseCache.remove(fromValue(cr.get()));
263                                            doFallBack=false;
264                                    } else {
265                                            final Object o=fromValue(cr.get());
266                                            final long et=cr.expires;
267                                            final long time=cr.time;
268                                            return new Entry() {
269    
270                                                    public long getExpirationTime() {
271                                                            return et;
272                                                    }
273    
274                                                    public long getTime() {
275                                                            return time;
276                                                    }
277    
278                                                    public Object get() {
279                                                            return o;
280                                                    }
281                                                    
282                                            };
283                                    }
284                            } 
285                            
286                            if (doFallBack && fallBack!=null) {
287                                    Entry entry = fallBack.get(key);
288                                    if (entry!=null) {
289                                            cache.put(key, new CacheReference(key, toValue(key, entry.get(), entry.getTime(), entry.getExpirationTime()), entry.getTime(), entry.getExpirationTime()));
290                                            reverseCache.put(entry.get(), key);
291                                            return entry;
292                                    }
293                            }
294                            
295                            if (key instanceof ProducingKey) {
296                                    Entry entry = ((ProducingKey) key).get();
297                                    cache.put(key, new CacheReference(key, toValue(key, entry.get(), entry.getTime(), entry.getExpirationTime()), entry.getTime(), entry.getExpirationTime()));
298                                    reverseCache.put(entry.get(),key);
299                                    addMeasurement("produce", 1, now);
300                                    return entry;
301                            }
302                            
303                            if (producer!=null) {
304                                    Entry entry = producer.get(key);
305                                    cache.put(key, new CacheReference(key, toValue(key, entry.get(), entry.getTime(), entry.getExpirationTime()), entry.getTime(), entry.getExpirationTime()));
306                                    reverseCache.put(entry.get(),key);
307                                    addMeasurement("produce", 1, now);
308                                    return entry;
309                            }
310                            
311                            return null;
312                    }
313            }
314            
315            public void clear() {
316                    synchronized (cache) {
317                            cache.clear();
318                            reverseCache.clear();
319                            
320                            if (fallBack!=null) {
321                                    fallBack.clear();
322                            }
323                    }
324            }
325            
326            public void remove(Object key) {
327                    synchronized (cache) {
328                            CacheReference cr = (CacheReference) cache.remove(key);
329                            if (cr!=null) {
330                                    reverseCache.remove(fromValue(cr.get()));
331                            }
332                    }
333                    addMeasurement("remove", 1);
334                    onRemove(key);
335            }
336    
337            public void remove(Acceptor acceptor) {
338                    synchronized (cache) {
339                            Iterator it=cache.keySet().iterator();
340                            int removed=0;
341                            while (it.hasNext()) {
342                                    Object key=it.next();
343                                    if (acceptor.accept(key)) {
344                                            Entry entry=(Entry) cache.get(key);
345                                            if (entry!=null) {
346                                                    reverseCache.remove(fromValue(entry.get()));
347                                            }
348                                            it.remove();
349                                            removed++;
350                                            onRemove(key);
351                                    }
352                            }
353                            if (removed>0) {
354                                    addMeasurement("remove", removed);
355                            }
356                    }
357            }
358            
359            private boolean shutDown=false;
360            
361            private void checkShutdown() {
362                    if (shutDown) {
363                            throw new IllegalStateException("Shut down");
364                    }
365            }
366    
367            public void stop() {
368                    if (!shutDown) {                        
369                            cache.clear();
370                            reverseCache.clear();
371                            referenceThread.interrupt();
372                            if (janitorTask!=null) {
373                                    janitorTask.cancel();
374                            }
375                            if (isOwnTimer && timer!=null) {
376                                    timer.cancel();
377                            }
378                            shutDown=true;
379                    }
380            }
381            
382            protected void finalize() throws Throwable {
383                    if (isActive()) {
384                            stop();
385                    }
386                    super.finalize();
387            }
388    
389            public Set keySet() {
390                    HashSet ret = new HashSet();
391                    synchronized (cache) {
392                            ret.addAll(cache.keySet());
393                    }
394                    
395                    if (producer!=null) {
396                            Set pkeys=producer.keySet();
397                            if (pkeys!=null) {
398                                    ret.addAll(pkeys);
399                            }
400                    }
401                    
402                    if (fallBack!=null) {
403                            Set fkeys=fallBack.keySet();
404                            if (fkeys!=null) {
405                                    ret.addAll(fkeys);
406                            }
407                    }
408                    
409                    return ret;
410            }
411    
412            public boolean isActive() {
413                    return !shutDown;
414            }
415    
416            /**
417             * Creates timer if neccessary, creates cleanup task and schedules it. 
418             */
419            public void start() throws ConfigurationException {
420                    if (timer==null) {
421                            timer=new Timer(true);
422                            isOwnTimer=true;
423                    }
424                    
425                    janitorTask=new JanitorTask(cache, reverseCache, measurementCategory);
426                    timer.schedule(janitorTask, cleanupInterval, cleanupInterval);
427            }
428    
429            public void setOwner(Object owner) {
430                    // TODO Nothing         
431            }
432    }
433    
434