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