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: */
020: package org.apache.mina.util;
021:
022: import java.util.Collection;
023: import java.util.Map;
024: import java.util.Set;
025: import java.util.concurrent.ConcurrentHashMap;
026: import java.util.concurrent.CopyOnWriteArrayList;
027: import java.util.concurrent.locks.ReadWriteLock;
028: import 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 The Apache MINA Project (dev@mina.apache.org)
036: * @version $Rev: 602315 $, $Date: 2007-12-07 21:07:26 -0700 (Fri, 07 Dec 2007) $
037: */
038: public class ExpiringMap<K, V> implements Map<K, V> {
039:
040: /**
041: * The default value, 60
042: */
043: public static final int DEFAULT_TIME_TO_LIVE = 60;
044:
045: /**
046: * The default value, 1
047: */
048: public static final int DEFAULT_EXPIRATION_INTERVAL = 1;
049:
050: private static volatile int expirerCount = 1;
051:
052: private final ConcurrentHashMap<K, ExpiringObject> delegate;
053:
054: private final CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners;
055:
056: private final Expirer expirer;
057:
058: /**
059: * Creates a new instance of ExpiringMap using the default values
060: * DEFAULT_TIME_TO_LIVE and DEFAULT_EXPIRATION_INTERVAL
061: *
062: */
063: public ExpiringMap() {
064: this (DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL);
065: }
066:
067: /**
068: * Creates a new instance of ExpiringMap using the supplied
069: * time-to-live value and the default value for DEFAULT_EXPIRATION_INTERVAL
070: *
071: * @param timeToLive
072: * The time-to-live value (seconds)
073: */
074: public ExpiringMap(int timeToLive) {
075: this (timeToLive, DEFAULT_EXPIRATION_INTERVAL);
076: }
077:
078: /**
079: * Creates a new instance of ExpiringMap using the supplied values and
080: * a {@link ConcurrentHashMap} for the internal data structure.
081: *
082: * @param timeToLive
083: * The time-to-live value (seconds)
084: * @param expirationInterval
085: * The time between checks to see if a value should be removed (seconds)
086: */
087: public ExpiringMap(int timeToLive, int expirationInterval) {
088: this (new ConcurrentHashMap<K, ExpiringObject>(),
089: new CopyOnWriteArrayList<ExpirationListener<V>>(),
090: timeToLive, expirationInterval);
091: }
092:
093: private ExpiringMap(
094: ConcurrentHashMap<K, ExpiringObject> delegate,
095: CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners,
096: int timeToLive, int expirationInterval) {
097: this .delegate = delegate;
098: this .expirationListeners = expirationListeners;
099:
100: this .expirer = new Expirer();
101: expirer.setTimeToLive(timeToLive);
102: expirer.setExpirationInterval(expirationInterval);
103: }
104:
105: public V put(K key, V value) {
106: ExpiringObject answer = delegate.put(key, new ExpiringObject(
107: key, value, System.currentTimeMillis()));
108: if (answer == null) {
109: return null;
110: }
111:
112: return answer.getValue();
113: }
114:
115: public V get(Object key) {
116: ExpiringObject object = delegate.get(key);
117:
118: if (object != null) {
119: object.setLastAccessTime(System.currentTimeMillis());
120:
121: return object.getValue();
122: }
123:
124: return null;
125: }
126:
127: public V remove(Object key) {
128: ExpiringObject answer = delegate.remove(key);
129: if (answer == null) {
130: return null;
131: }
132:
133: return answer.getValue();
134: }
135:
136: public boolean containsKey(Object key) {
137: return delegate.containsKey(key);
138: }
139:
140: public boolean containsValue(Object value) {
141: return delegate.containsValue(value);
142: }
143:
144: public int size() {
145: return delegate.size();
146: }
147:
148: public boolean isEmpty() {
149: return delegate.isEmpty();
150: }
151:
152: public void clear() {
153: delegate.clear();
154: }
155:
156: @Override
157: public int hashCode() {
158: return delegate.hashCode();
159: }
160:
161: public Set<K> keySet() {
162: return delegate.keySet();
163: }
164:
165: @Override
166: public boolean equals(Object obj) {
167: return delegate.equals(obj);
168: }
169:
170: public void putAll(Map<? extends K, ? extends V> inMap) {
171: for (Entry<? extends K, ? extends V> e : inMap.entrySet()) {
172: this .put(e.getKey(), e.getValue());
173: }
174: }
175:
176: public Collection<V> values() {
177: throw new UnsupportedOperationException();
178: }
179:
180: public Set<Map.Entry<K, V>> entrySet() {
181: throw new UnsupportedOperationException();
182: }
183:
184: public void addExpirationListener(ExpirationListener<V> listener) {
185: expirationListeners.add(listener);
186: }
187:
188: public void removeExpirationListener(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: }
|