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   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
36   */
37  public class ExpiringMap<K, V> implements Map<K, V> {
38      /** The default value, 60 seconds */
39      public static final int DEFAULT_TIME_TO_LIVE = 60;
40  
41      /** The default value, 1 second */
42      public static final int DEFAULT_EXPIRATION_INTERVAL = 1;
43  
44      private static volatile int expirerCount = 1;
45  
46      private final ConcurrentHashMap<K, ExpiringObject> delegate;
47  
48      private final CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners;
49  
50      private final Expirer expirer;
51  
52      /**
53       * Creates a new instance of ExpiringMap using the default values 
54       * DEFAULT_TIME_TO_LIVE and DEFAULT_EXPIRATION_INTERVAL
55       *
56       */
57      public ExpiringMap() {
58          this(DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL);
59      }
60  
61      /**
62       * Creates a new instance of ExpiringMap using the supplied 
63       * time-to-live value and the default value for DEFAULT_EXPIRATION_INTERVAL
64       *
65       * @param timeToLive The time-to-live value (seconds)
66       */
67      public ExpiringMap(int timeToLive) {
68          this(timeToLive, DEFAULT_EXPIRATION_INTERVAL);
69      }
70  
71      /**
72       * Creates a new instance of ExpiringMap using the supplied values and 
73       * a {@link ConcurrentHashMap} for the internal data structure.
74       *
75       * @param timeToLive The time-to-live value (seconds)
76       * @param expirationInterval The time between checks to see if a value should be removed (seconds)
77       */
78      public ExpiringMap(int timeToLive, int expirationInterval) {
79          this(new ConcurrentHashMap<K, ExpiringObject>(), new CopyOnWriteArrayList<ExpirationListener<V>>(), timeToLive,
80                  expirationInterval);
81      }
82  
83      private ExpiringMap(ConcurrentHashMap<K, ExpiringObject> delegate,
84              CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners, int timeToLive, int expirationInterval) {
85          this.delegate = delegate;
86          this.expirationListeners = expirationListeners;
87  
88          this.expirer = new Expirer();
89          expirer.setTimeToLive(timeToLive);
90          expirer.setExpirationInterval(expirationInterval);
91      }
92  
93      public V put(K key, V value) {
94          ExpiringObject answer = delegate.put(key, new ExpiringObject(key, value, System.currentTimeMillis()));
95          
96          if (answer == null) {
97              return null;
98          }
99  
100         return answer.getValue();
101     }
102 
103     public V get(Object key) {
104         ExpiringObject object = delegate.get(key);
105 
106         if (object != null) {
107             object.setLastAccessTime(System.currentTimeMillis());
108 
109             return object.getValue();
110         }
111 
112         return null;
113     }
114 
115     public V remove(Object key) {
116         ExpiringObject answer = delegate.remove(key);
117         if (answer == null) {
118             return null;
119         }
120 
121         return answer.getValue();
122     }
123 
124     public boolean containsKey(Object key) {
125         return delegate.containsKey(key);
126     }
127 
128     public boolean containsValue(Object value) {
129         return delegate.containsValue(value);
130     }
131 
132     public int size() {
133         return delegate.size();
134     }
135 
136     public boolean isEmpty() {
137         return delegate.isEmpty();
138     }
139 
140     public void clear() {
141         delegate.clear();
142     }
143 
144     @Override
145     public int hashCode() {
146         return delegate.hashCode();
147     }
148 
149     public Set<K> keySet() {
150         return delegate.keySet();
151     }
152 
153     @Override
154     public boolean equals(Object obj) {
155         return delegate.equals(obj);
156     }
157 
158     public void putAll(Map<? extends K, ? extends V> inMap) {
159         for (Entry<? extends K, ? extends V> e : inMap.entrySet()) {
160             this.put(e.getKey(), e.getValue());
161         }
162     }
163 
164     public Collection<V> values() {
165         throw new UnsupportedOperationException();
166     }
167 
168     public Set<Map.Entry<K, V>> entrySet() {
169         throw new UnsupportedOperationException();
170     }
171 
172     public void addExpirationListener(ExpirationListener<V> listener) {
173         expirationListeners.add(listener);
174     }
175 
176     public void removeExpirationListener(ExpirationListener<V> listener) {
177         expirationListeners.remove(listener);
178     }
179 
180     public Expirer getExpirer() {
181         return expirer;
182     }
183 
184     public int getExpirationInterval() {
185         return expirer.getExpirationInterval();
186     }
187 
188     public int getTimeToLive() {
189         return expirer.getTimeToLive();
190     }
191 
192     public void setExpirationInterval(int expirationInterval) {
193         expirer.setExpirationInterval(expirationInterval);
194     }
195 
196     public void setTimeToLive(int timeToLive) {
197         expirer.setTimeToLive(timeToLive);
198     }
199 
200     private class ExpiringObject {
201         private K key;
202 
203         private V value;
204 
205         private long lastAccessTime;
206 
207         private final ReadWriteLock lastAccessTimeLock = new ReentrantReadWriteLock();
208 
209         ExpiringObject(K key, V value, long lastAccessTime) {
210             if (value == null) {
211                 throw new IllegalArgumentException("An expiring object cannot be null.");
212             }
213 
214             this.key = key;
215             this.value = value;
216             this.lastAccessTime = lastAccessTime;
217         }
218 
219         public long getLastAccessTime() {
220             lastAccessTimeLock.readLock().lock();
221 
222             try {
223                 return lastAccessTime;
224             } finally {
225                 lastAccessTimeLock.readLock().unlock();
226             }
227         }
228 
229         public void setLastAccessTime(long lastAccessTime) {
230             lastAccessTimeLock.writeLock().lock();
231 
232             try {
233                 this.lastAccessTime = lastAccessTime;
234             } finally {
235                 lastAccessTimeLock.writeLock().unlock();
236             }
237         }
238 
239         public K getKey() {
240             return key;
241         }
242 
243         public V getValue() {
244             return value;
245         }
246 
247         @Override
248         public boolean equals(Object obj) {
249             return value.equals(obj);
250         }
251 
252         @Override
253         public int hashCode() {
254             return value.hashCode();
255         }
256     }
257 
258     /**
259      * A Thread that monitors an {@link ExpiringMap} and will remove
260      * elements that have passed the threshold.
261      *
262      */
263     public class Expirer implements Runnable {
264         private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
265 
266         private long timeToLiveMillis;
267 
268         private long expirationIntervalMillis;
269 
270         private boolean running = false;
271 
272         private final Thread expirerThread;
273 
274         /**
275          * Creates a new instance of Expirer.  
276          *
277          */
278         public Expirer() {
279             expirerThread = new Thread(this, "ExpiringMapExpirer-" + expirerCount++);
280             expirerThread.setDaemon(true);
281         }
282 
283         public void run() {
284             while (running) {
285                 processExpires();
286 
287                 try {
288                     Thread.sleep(expirationIntervalMillis);
289                 } catch (InterruptedException e) {
290                     // Do nothing
291                 }
292             }
293         }
294 
295         private void processExpires() {
296             long timeNow = System.currentTimeMillis();
297 
298             for (ExpiringObject o : delegate.values()) {
299 
300                 if (timeToLiveMillis <= 0) {
301                     continue;
302                 }
303 
304                 long timeIdle = timeNow - o.getLastAccessTime();
305 
306                 if (timeIdle >= timeToLiveMillis) {
307                     delegate.remove(o.getKey());
308 
309                     for (ExpirationListener<V> listener : expirationListeners) {
310                         listener.expired(o.getValue());
311                     }
312                 }
313             }
314         }
315 
316         /**
317          * Kick off this thread which will look for old objects and remove them.
318          *
319          */
320         public void startExpiring() {
321             stateLock.writeLock().lock();
322 
323             try {
324                 if (!running) {
325                     running = true;
326                     expirerThread.start();
327                 }
328             } finally {
329                 stateLock.writeLock().unlock();
330             }
331         }
332 
333         /**
334          * If this thread has not started, then start it.  
335          * Otherwise just return;
336          */
337         public void startExpiringIfNotStarted() {
338             stateLock.readLock().lock();
339             try {
340                 if (running) {
341                     return;
342                 }
343             } finally {
344                 stateLock.readLock().unlock();
345             }
346 
347             stateLock.writeLock().lock();
348             try {
349                 if (!running) {
350                     running = true;
351                     expirerThread.start();
352                 }
353             } finally {
354                 stateLock.writeLock().unlock();
355             }
356         }
357 
358         /**
359          * Stop the thread from monitoring the map.
360          */
361         public void stopExpiring() {
362             stateLock.writeLock().lock();
363 
364             try {
365                 if (running) {
366                     running = false;
367                     expirerThread.interrupt();
368                 }
369             } finally {
370                 stateLock.writeLock().unlock();
371             }
372         }
373 
374         /**
375          * Checks to see if the thread is running
376          *
377          * @return
378          *  If the thread is running, true.  Otherwise false.
379          */
380         public boolean isRunning() {
381             stateLock.readLock().lock();
382 
383             try {
384                 return running;
385             } finally {
386                 stateLock.readLock().unlock();
387             }
388         }
389 
390         /**
391          * @return the Time-to-live value in seconds.
392          */
393         public int getTimeToLive() {
394             stateLock.readLock().lock();
395 
396             try {
397                 return (int) timeToLiveMillis / 1000;
398             } finally {
399                 stateLock.readLock().unlock();
400             }
401         }
402 
403         /**
404          * Update the value for the time-to-live
405          *
406          * @param timeToLive
407          *  The time-to-live (seconds)
408          */
409         public void setTimeToLive(long timeToLive) {
410             stateLock.writeLock().lock();
411 
412             try {
413                 this.timeToLiveMillis = timeToLive * 1000;
414             } finally {
415                 stateLock.writeLock().unlock();
416             }
417         }
418 
419         /**
420          * Get the interval in which an object will live in the map before
421          * it is removed.
422          *
423          * @return
424          *  The time in seconds.
425          */
426         public int getExpirationInterval() {
427             stateLock.readLock().lock();
428 
429             try {
430                 return (int) expirationIntervalMillis / 1000;
431             } finally {
432                 stateLock.readLock().unlock();
433             }
434         }
435 
436         /**
437          * Set the interval in which an object will live in the map before
438          * it is removed.
439          *
440          * @param expirationInterval
441          *  The time in seconds
442          */
443         public void setExpirationInterval(long expirationInterval) {
444             stateLock.writeLock().lock();
445 
446             try {
447                 this.expirationIntervalMillis = expirationInterval * 1000;
448             } finally {
449                 stateLock.writeLock().unlock();
450             }
451         }
452     }
453 }