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