View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.mina.util;
21  
22  import java.util.Collection;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.concurrent.ConcurrentHashMap;
26  import java.util.concurrent.CopyOnWriteArrayList;
27  import java.util.concurrent.locks.ReadWriteLock;
28  import java.util.concurrent.locks.ReentrantReadWriteLock;
29  
30  /**
31   * A map with expiration.  This class contains a worker thread that will 
32   * periodically check this class in order to determine if any objects 
33   * should be removed based on the provided time-to-live value.
34   * 
35   * @param <K> The key type
36   * @param <V> The value type
37   *
38   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
39   */
40  public class ExpiringMap<K, V> implements Map<K, V> {
41      /** The default value, 60 seconds */
42      public static final int DEFAULT_TIME_TO_LIVE = 60;
43  
44      /** The default value, 1 second */
45      public static final int DEFAULT_EXPIRATION_INTERVAL = 1;
46  
47      private static volatile int expirerCount = 1;
48  
49      private final ConcurrentHashMap<K, ExpiringObject> delegate;
50  
51      private final CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners;
52  
53      private final Expirer expirer;
54  
55      /**
56       * Creates a new instance of ExpiringMap using the default values 
57       * DEFAULT_TIME_TO_LIVE and DEFAULT_EXPIRATION_INTERVAL
58       *
59       */
60      public ExpiringMap() {
61          this(DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL);
62      }
63  
64      /**
65       * Creates a new instance of ExpiringMap using the supplied 
66       * time-to-live value and the default value for DEFAULT_EXPIRATION_INTERVAL
67       *
68       * @param timeToLive The time-to-live value (seconds)
69       */
70      public ExpiringMap(int timeToLive) {
71          this(timeToLive, DEFAULT_EXPIRATION_INTERVAL);
72      }
73  
74      /**
75       * Creates a new instance of ExpiringMap using the supplied values and 
76       * a {@link ConcurrentHashMap} for the internal data structure.
77       *
78       * @param timeToLive The time-to-live value (seconds)
79       * @param expirationInterval The time between checks to see if a value should be removed (seconds)
80       */
81      public ExpiringMap(int timeToLive, int expirationInterval) {
82          this(new ConcurrentHashMap<K, ExpiringObject>(), new CopyOnWriteArrayList<ExpirationListener<V>>(), timeToLive,
83                  expirationInterval);
84      }
85  
86      private ExpiringMap(ConcurrentHashMap<K, ExpiringObject> delegate,
87              CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners, int timeToLive, int expirationInterval) {
88          this.delegate = delegate;
89          this.expirationListeners = expirationListeners;
90  
91          this.expirer = new Expirer();
92          expirer.setTimeToLive(timeToLive);
93          expirer.setExpirationInterval(expirationInterval);
94      }
95  
96      /**
97       * {@inheritDoc}
98       */
99      @Override
100     public V put(K key, V value) {
101         ExpiringObject answer = delegate.put(key, new ExpiringObject(key, value, System.currentTimeMillis()));
102         
103         if (answer == null) {
104             return null;
105         }
106 
107         return answer.getValue();
108     }
109 
110     /**
111      * {@inheritDoc}
112      */
113     @Override
114     public V get(Object key) {
115         ExpiringObject object = delegate.get(key);
116 
117         if (object != null) {
118             object.setLastAccessTime(System.currentTimeMillis());
119 
120             return object.getValue();
121         }
122 
123         return null;
124     }
125 
126     /**
127      * {@inheritDoc}
128      */
129     @Override
130     public V remove(Object key) {
131         ExpiringObject answer = delegate.remove(key);
132         if (answer == null) {
133             return null;
134         }
135 
136         return answer.getValue();
137     }
138 
139     /**
140      * {@inheritDoc}
141      */
142     @Override
143     public boolean containsKey(Object key) {
144         return delegate.containsKey(key);
145     }
146 
147     /**
148      * {@inheritDoc}
149      */
150     @Override
151     public boolean containsValue(Object value) {
152         return delegate.containsValue(value);
153     }
154 
155     /**
156      * {@inheritDoc}
157      */
158     @Override
159     public int size() {
160         return delegate.size();
161     }
162 
163     /**
164      * {@inheritDoc}
165      */
166     @Override
167     public boolean isEmpty() {
168         return delegate.isEmpty();
169     }
170 
171     /**
172      * {@inheritDoc}
173      */
174     @Override
175     public void clear() {
176         delegate.clear();
177     }
178 
179     /**
180      * {@inheritDoc}
181      */
182     @Override
183     public int hashCode() {
184         return delegate.hashCode();
185     }
186 
187     /**
188      * {@inheritDoc}
189      */
190     @Override
191     public Set<K> keySet() {
192         return delegate.keySet();
193     }
194 
195     /**
196      * {@inheritDoc}
197      */
198     @Override
199     public boolean equals(Object obj) {
200         return delegate.equals(obj);
201     }
202 
203     /**
204      * {@inheritDoc}
205      */
206     @Override
207     public void putAll(Map<? extends K, ? extends V> inMap) {
208         for (Entry<? extends K, ? extends V> e : inMap.entrySet()) {
209             this.put(e.getKey(), e.getValue());
210         }
211     }
212 
213     /**
214      * {@inheritDoc}
215      */
216     @Override
217     public Collection<V> values() {
218         throw new UnsupportedOperationException();
219     }
220 
221     /**
222      * {@inheritDoc}
223      */
224     @Override
225     public Set<Map.Entry<K, V>> entrySet() {
226         throw new UnsupportedOperationException();
227     }
228 
229     /**
230      * Adds a listener in the expiration listeners
231      * 
232      * @param listener The listener to add
233      */
234     public void addExpirationListener(ExpirationListener<V> listener) {
235         expirationListeners.add(listener);
236     }
237 
238     /**
239      * Removes a listener from the expiration listeners
240      * 
241      * @param listener The listener to remove
242      */
243     public void removeExpirationListener(ExpirationListener<V> listener) {
244         expirationListeners.remove(listener);
245     }
246 
247     /**
248      * @return The Expirer instance
249      */
250     public Expirer getExpirer() {
251         return expirer;
252     }
253 
254     /**
255      * Get the interval in which an object will live in the map before it is removed.
256      * 
257      * @return The expiration time in second
258      */
259     public int getExpirationInterval() {
260         return expirer.getExpirationInterval();
261     }
262 
263     /**
264      * @return the Time-to-live value in seconds.
265      */
266     public int getTimeToLive() {
267         return expirer.getTimeToLive();
268     }
269 
270     /**
271      * Set the interval in which an object will live in the map before it is removed.
272      * 
273      * @param expirationInterval The expiration time in seconds
274      */
275     public void setExpirationInterval(int expirationInterval) {
276         expirer.setExpirationInterval(expirationInterval);
277     }
278 
279     /**
280      * Update the value for the time-to-live
281      *
282      * @param timeToLive The time-to-live (seconds)
283      */
284     public void setTimeToLive(int timeToLive) {
285         expirer.setTimeToLive(timeToLive);
286     }
287 
288     private class ExpiringObject {
289         private K key;
290 
291         private V value;
292 
293         private long lastAccessTime;
294 
295         private final ReadWriteLock lastAccessTimeLock = new ReentrantReadWriteLock();
296 
297         ExpiringObject(K key, V value, long lastAccessTime) {
298             if (value == null) {
299                 throw new IllegalArgumentException("An expiring object cannot be null.");
300             }
301 
302             this.key = key;
303             this.value = value;
304             this.lastAccessTime = lastAccessTime;
305         }
306 
307         public long getLastAccessTime() {
308             lastAccessTimeLock.readLock().lock();
309 
310             try {
311                 return lastAccessTime;
312             } finally {
313                 lastAccessTimeLock.readLock().unlock();
314             }
315         }
316 
317         public void setLastAccessTime(long lastAccessTime) {
318             lastAccessTimeLock.writeLock().lock();
319 
320             try {
321                 this.lastAccessTime = lastAccessTime;
322             } finally {
323                 lastAccessTimeLock.writeLock().unlock();
324             }
325         }
326 
327         public K getKey() {
328             return key;
329         }
330 
331         public V getValue() {
332             return value;
333         }
334 
335         @Override
336         public boolean equals(Object obj) {
337             return value.equals(obj);
338         }
339 
340         @Override
341         public int hashCode() {
342             return value.hashCode();
343         }
344     }
345 
346     /**
347      * A Thread that monitors an {@link ExpiringMap} and will remove
348      * elements that have passed the threshold.
349      *
350      */
351     public class Expirer implements Runnable {
352         private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
353 
354         private long timeToLiveMillis;
355 
356         private long expirationIntervalMillis;
357 
358         private boolean running = false;
359 
360         private final Thread expirerThread;
361 
362         /**
363          * Creates a new instance of Expirer.  
364          *
365          */
366         public Expirer() {
367             expirerThread = new Thread(this, "ExpiringMapExpirer-" + expirerCount++);
368             expirerThread.setDaemon(true);
369         }
370 
371         /**
372          * {@inheritDoc}
373          */
374         @Override
375         public void run() {
376             while (running) {
377                 processExpires();
378 
379                 try {
380                     Thread.sleep(expirationIntervalMillis);
381                 } catch (InterruptedException e) {
382                     // Do nothing
383                 }
384             }
385         }
386 
387         private void processExpires() {
388             long timeNow = System.currentTimeMillis();
389 
390             for (ExpiringObject o : delegate.values()) {
391 
392                 if (timeToLiveMillis <= 0) {
393                     continue;
394                 }
395 
396                 long timeIdle = timeNow - o.getLastAccessTime();
397 
398                 if (timeIdle >= timeToLiveMillis) {
399                     delegate.remove(o.getKey());
400 
401                     for (ExpirationListener<V> listener : expirationListeners) {
402                         listener.expired(o.getValue());
403                     }
404                 }
405             }
406         }
407 
408         /**
409          * Kick off this thread which will look for old objects and remove them.
410          *
411          */
412         public void startExpiring() {
413             stateLock.writeLock().lock();
414 
415             try {
416                 if (!running) {
417                     running = true;
418                     expirerThread.start();
419                 }
420             } finally {
421                 stateLock.writeLock().unlock();
422             }
423         }
424 
425         /**
426          * If this thread has not started, then start it.  
427          * Otherwise just return;
428          */
429         public void startExpiringIfNotStarted() {
430             stateLock.readLock().lock();
431             
432             try {
433                 if (running) {
434                     return;
435                 }
436             } finally {
437                 stateLock.readLock().unlock();
438             }
439 
440             stateLock.writeLock().lock();
441             
442             try {
443                 if (!running) {
444                     running = true;
445                     expirerThread.start();
446                 }
447             } finally {
448                 stateLock.writeLock().unlock();
449             }
450         }
451 
452         /**
453          * Stop the thread from monitoring the map.
454          */
455         public void stopExpiring() {
456             stateLock.writeLock().lock();
457 
458             try {
459                 if (running) {
460                     running = false;
461                     expirerThread.interrupt();
462                 }
463             } finally {
464                 stateLock.writeLock().unlock();
465             }
466         }
467 
468         /**
469          * Checks to see if the thread is running
470          *
471          * @return
472          *  If the thread is running, true.  Otherwise false.
473          */
474         public boolean isRunning() {
475             stateLock.readLock().lock();
476 
477             try {
478                 return running;
479             } finally {
480                 stateLock.readLock().unlock();
481             }
482         }
483 
484         /**
485          * @return the Time-to-live value in seconds.
486          */
487         public int getTimeToLive() {
488             stateLock.readLock().lock();
489 
490             try {
491                 return (int) timeToLiveMillis / 1000;
492             } finally {
493                 stateLock.readLock().unlock();
494             }
495         }
496 
497         /**
498          * Update the value for the time-to-live
499          *
500          * @param timeToLive
501          *  The time-to-live (seconds)
502          */
503         public void setTimeToLive(long timeToLive) {
504             stateLock.writeLock().lock();
505 
506             try {
507                 this.timeToLiveMillis = timeToLive * 1000;
508             } finally {
509                 stateLock.writeLock().unlock();
510             }
511         }
512 
513         /**
514          * Get the interval in which an object will live in the map before
515          * it is removed.
516          *
517          * @return
518          *  The time in seconds.
519          */
520         public int getExpirationInterval() {
521             stateLock.readLock().lock();
522 
523             try {
524                 return (int) expirationIntervalMillis / 1000;
525             } finally {
526                 stateLock.readLock().unlock();
527             }
528         }
529 
530         /**
531          * Set the interval in which an object will live in the map before
532          * it is removed.
533          *
534          * @param expirationInterval
535          *  The time in seconds
536          */
537         public void setExpirationInterval(long expirationInterval) {
538             stateLock.writeLock().lock();
539 
540             try {
541                 this.expirationIntervalMillis = expirationInterval * 1000;
542             } finally {
543                 stateLock.writeLock().unlock();
544             }
545         }
546     }
547 }