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: 602315 $, $Date: 2007-12-07 21:07:26 -0700 (Fri, 07 Dec 2007) $
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      * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
276      * @version $Rev: 602315 $, $Date: 2007-12-07 21:07:26 -0700 (Fri, 07 Dec 2007) $
277      */
278     public class Expirer implements Runnable {
279         private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
280 
281         private long timeToLiveMillis;
282 
283         private long expirationIntervalMillis;
284 
285         private boolean running = false;
286 
287         private final Thread expirerThread;
288 
289         /**
290          * Creates a new instance of Expirer.  
291          *
292          */
293         public Expirer() {
294             expirerThread = new Thread(this, "ExpiringMapExpirer-"
295                     + expirerCount++);
296             expirerThread.setDaemon(true);
297         }
298 
299         public void run() {
300             while (running) {
301                 processExpires();
302 
303                 try {
304                     Thread.sleep(expirationIntervalMillis);
305                 } catch (InterruptedException e) {
306                 }
307             }
308         }
309 
310         private void processExpires() {
311             long timeNow = System.currentTimeMillis();
312 
313             for (ExpiringObject o : delegate.values()) {
314 
315                 if (timeToLiveMillis <= 0) {
316                     continue;
317                 }
318 
319                 long timeIdle = timeNow - o.getLastAccessTime();
320 
321                 if (timeIdle >= timeToLiveMillis) {
322                     delegate.remove(o.getKey());
323 
324                     for (ExpirationListener<V> listener : expirationListeners) {
325                         listener.expired(o.getValue());
326                     }
327                 }
328             }
329         }
330 
331         /**
332          * Kick off this thread which will look for old objects and remove them.
333          *
334          */
335         public void startExpiring() {
336             stateLock.writeLock().lock();
337 
338             try {
339                 if (!running) {
340                     running = true;
341                     expirerThread.start();
342                 }
343             } finally {
344                 stateLock.writeLock().unlock();
345             }
346         }
347 
348         /**
349          * If this thread has not started, then start it.  
350          * Otherwise just return;
351          */
352         public void startExpiringIfNotStarted() {
353             stateLock.readLock().lock();
354             try {
355                 if (running) {
356                     return;
357                 }
358             } finally {
359                 stateLock.readLock().unlock();
360             }
361 
362             stateLock.writeLock().lock();
363             try {
364                 if (!running) {
365                     running = true;
366                     expirerThread.start();
367                 }
368             } finally {
369                 stateLock.writeLock().unlock();
370             }
371         }
372 
373         /**
374          * Stop the thread from monitoring the map.
375          */
376         public void stopExpiring() {
377             stateLock.writeLock().lock();
378 
379             try {
380                 if (running) {
381                     running = false;
382                     expirerThread.interrupt();
383                 }
384             } finally {
385                 stateLock.writeLock().unlock();
386             }
387         }
388 
389         /**
390          * Checks to see if the thread is running
391          *
392          * @return
393          *  If the thread is running, true.  Otherwise false.
394          */
395         public boolean isRunning() {
396             stateLock.readLock().lock();
397 
398             try {
399                 return running;
400             } finally {
401                 stateLock.readLock().unlock();
402             }
403         }
404 
405         /**
406          * Returns the Time-to-live value.
407          *
408          * @return
409          *  The time-to-live (seconds)
410          */
411         public int getTimeToLive() {
412             stateLock.readLock().lock();
413 
414             try {
415                 return (int) timeToLiveMillis / 1000;
416             } finally {
417                 stateLock.readLock().unlock();
418             }
419         }
420 
421         /**
422          * Update the value for the time-to-live
423          *
424          * @param timeToLive
425          *  The time-to-live (seconds)
426          */
427         public void setTimeToLive(long timeToLive) {
428             stateLock.writeLock().lock();
429 
430             try {
431                 this.timeToLiveMillis = timeToLive * 1000;
432             } finally {
433                 stateLock.writeLock().unlock();
434             }
435         }
436 
437         /**
438          * Get the interval in which an object will live in the map before
439          * it is removed.
440          *
441          * @return
442          *  The time in seconds.
443          */
444         public int getExpirationInterval() {
445             stateLock.readLock().lock();
446 
447             try {
448                 return (int) expirationIntervalMillis / 1000;
449             } finally {
450                 stateLock.readLock().unlock();
451             }
452         }
453 
454         /**
455          * Set the interval in which an object will live in the map before
456          * it is removed.
457          *
458          * @param expirationInterval
459          *  The time in seconds
460          */
461         public void setExpirationInterval(long expirationInterval) {
462             stateLock.writeLock().lock();
463 
464             try {
465                 this.expirationIntervalMillis = expirationInterval * 1000;
466             } finally {
467                 stateLock.writeLock().unlock();
468             }
469         }
470     }
471 }