001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.util;
021
022import java.util.Collection;
023import java.util.Map;
024import java.util.Set;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.concurrent.CopyOnWriteArrayList;
027import java.util.concurrent.locks.ReadWriteLock;
028import java.util.concurrent.locks.ReentrantReadWriteLock;
029
030/**
031 * A map with expiration.  This class contains a worker thread that will 
032 * periodically check this class in order to determine if any objects 
033 * should be removed based on the provided time-to-live value.
034 *
035 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
036 */
037public class ExpiringMap<K, V> implements Map<K, V> {
038    /** The default value, 60 seconds */
039    public static final int DEFAULT_TIME_TO_LIVE = 60;
040
041    /** The default value, 1 second */
042    public static final int DEFAULT_EXPIRATION_INTERVAL = 1;
043
044    private static volatile int expirerCount = 1;
045
046    private final ConcurrentHashMap<K, ExpiringObject> delegate;
047
048    private final CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners;
049
050    private final Expirer expirer;
051
052    /**
053     * Creates a new instance of ExpiringMap using the default values 
054     * DEFAULT_TIME_TO_LIVE and DEFAULT_EXPIRATION_INTERVAL
055     *
056     */
057    public ExpiringMap() {
058        this(DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL);
059    }
060
061    /**
062     * Creates a new instance of ExpiringMap using the supplied 
063     * time-to-live value and the default value for DEFAULT_EXPIRATION_INTERVAL
064     *
065     * @param timeToLive The time-to-live value (seconds)
066     */
067    public ExpiringMap(int timeToLive) {
068        this(timeToLive, DEFAULT_EXPIRATION_INTERVAL);
069    }
070
071    /**
072     * Creates a new instance of ExpiringMap using the supplied values and 
073     * a {@link ConcurrentHashMap} for the internal data structure.
074     *
075     * @param timeToLive The time-to-live value (seconds)
076     * @param expirationInterval The time between checks to see if a value should be removed (seconds)
077     */
078    public ExpiringMap(int timeToLive, int expirationInterval) {
079        this(new ConcurrentHashMap<K, ExpiringObject>(), new CopyOnWriteArrayList<ExpirationListener<V>>(), timeToLive,
080                expirationInterval);
081    }
082
083    private ExpiringMap(ConcurrentHashMap<K, ExpiringObject> delegate,
084            CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners, int timeToLive, int expirationInterval) {
085        this.delegate = delegate;
086        this.expirationListeners = expirationListeners;
087
088        this.expirer = new Expirer();
089        expirer.setTimeToLive(timeToLive);
090        expirer.setExpirationInterval(expirationInterval);
091    }
092
093    public V put(K key, V value) {
094        ExpiringObject answer = delegate.put(key, new ExpiringObject(key, value, System.currentTimeMillis()));
095        
096        if (answer == null) {
097            return null;
098        }
099
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}