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>(),
88                  new CopyOnWriteArrayList<ExpirationListener<V>>(), timeToLive,
89                  expirationInterval);
90      }
91  
92      private ExpiringMap(ConcurrentHashMap<K, ExpiringObject> delegate,
93              CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners,
94              int timeToLive, int expirationInterval) {
95          this.delegate = delegate;
96          this.expirationListeners = expirationListeners;
97  
98          this.expirer = new Expirer();
99          expirer.setTimeToLive(timeToLive);
100         expirer.setExpirationInterval(expirationInterval);
101     }
102 
103     public V put(K key, V value) {
104         ExpiringObject answer = delegate.put(key, new ExpiringObject(key,
105                 value, System.currentTimeMillis()));
106         if (answer == null) {
107             return null;
108         }
109 
110         return answer.getValue();
111     }
112 
113     public V get(Object key) {
114         ExpiringObject object = delegate.get(key);
115 
116         if (object != null) {
117             object.setLastAccessTime(System.currentTimeMillis());
118 
119             return object.getValue();
120         }
121 
122         return null;
123     }
124 
125     public V remove(Object key) {
126         ExpiringObject answer = delegate.remove(key);
127         if (answer == null) {
128             return null;
129         }
130 
131         return answer.getValue();
132     }
133 
134     public boolean containsKey(Object key) {
135         return delegate.containsKey(key);
136     }
137 
138     public boolean containsValue(Object value) {
139         return delegate.containsValue(value);
140     }
141 
142     public int size() {
143         return delegate.size();
144     }
145 
146     public boolean isEmpty() {
147         return delegate.isEmpty();
148     }
149 
150     public void clear() {
151         delegate.clear();
152     }
153 
154     @Override
155     public int hashCode() {
156         return delegate.hashCode();
157     }
158 
159     public Set<K> keySet() {
160         return delegate.keySet();
161     }
162 
163     @Override
164     public boolean equals(Object obj) {
165         return delegate.equals(obj);
166     }
167 
168     public void putAll(Map<? extends K, ? extends V> inMap) {
169         for (Entry<? extends K, ? extends V> e : inMap.entrySet()) {
170             this.put(e.getKey(), e.getValue());
171         }
172     }
173 
174     public Collection<V> values() {
175         throw new UnsupportedOperationException();
176     }
177 
178     public Set<Map.Entry<K, V>> entrySet() {
179         throw new UnsupportedOperationException();
180     }
181 
182     public void addExpirationListener(ExpirationListener<V> listener) {
183         expirationListeners.add(listener);
184     }
185 
186     public void removeExpirationListener(
187             ExpirationListener<V> listener) {
188         expirationListeners.remove(listener);
189     }
190 
191     public Expirer getExpirer() {
192         return expirer;
193     }
194 
195     public int getExpirationInterval() {
196         return expirer.getExpirationInterval();
197     }
198 
199     public int getTimeToLive() {
200         return expirer.getTimeToLive();
201     }
202 
203     public void setExpirationInterval(int expirationInterval) {
204         expirer.setExpirationInterval(expirationInterval);
205     }
206 
207     public void setTimeToLive(int timeToLive) {
208         expirer.setTimeToLive(timeToLive);
209     }
210 
211     private class ExpiringObject {
212         private K key;
213 
214         private V value;
215 
216         private long lastAccessTime;
217 
218         private final ReadWriteLock lastAccessTimeLock = new ReentrantReadWriteLock();
219 
220         ExpiringObject(K key, V value, long lastAccessTime) {
221             if (value == null) {
222                 throw new IllegalArgumentException(
223                         "An expiring object cannot be null.");
224             }
225 
226             this.key = key;
227             this.value = value;
228             this.lastAccessTime = lastAccessTime;
229         }
230 
231         public long getLastAccessTime() {
232             lastAccessTimeLock.readLock().lock();
233 
234             try {
235                 return lastAccessTime;
236             } finally {
237                 lastAccessTimeLock.readLock().unlock();
238             }
239         }
240 
241         public void setLastAccessTime(long lastAccessTime) {
242             lastAccessTimeLock.writeLock().lock();
243 
244             try {
245                 this.lastAccessTime = lastAccessTime;
246             } finally {
247                 lastAccessTimeLock.writeLock().unlock();
248             }
249         }
250 
251         public K getKey() {
252             return key;
253         }
254 
255         public V getValue() {
256             return value;
257         }
258 
259         @Override
260         public boolean equals(Object obj) {
261             return value.equals(obj);
262         }
263 
264         @Override
265         public int hashCode() {
266             return value.hashCode();
267         }
268     }
269 
270     /**
271      * A Thread that monitors an {@link ExpiringMap} and will remove
272      * elements that have passed the threshold.
273      *
274      */ 
275     public class Expirer implements Runnable {
276         private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
277 
278         private long timeToLiveMillis;
279 
280         private long expirationIntervalMillis;
281 
282         private boolean running = false;
283 
284         private final Thread expirerThread;
285 
286         /**
287          * Creates a new instance of Expirer.  
288          *
289          */
290         public Expirer() {
291             expirerThread = new Thread(this, "ExpiringMapExpirer-"
292                     + expirerCount++);
293             expirerThread.setDaemon(true);
294         }
295 
296         public void run() {
297             while (running) {
298                 processExpires();
299 
300                 try {
301                     Thread.sleep(expirationIntervalMillis);
302                 } catch (InterruptedException e) {
303                     // Do nothing
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 }