001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.collections4.map; 018 019import java.io.IOException; 020import java.io.ObjectInputStream; 021import java.io.ObjectOutputStream; 022import java.io.Serializable; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.Map; 027import java.util.Set; 028import java.util.concurrent.TimeUnit; 029 030/** 031 * Decorates a <code>Map</code> to evict expired entries once their expiration 032 * time has been reached. 033 * <p> 034 * When putting a key-value pair in the map this decorator uses a 035 * {@link ExpirationPolicy} to determine how long the entry should remain alive 036 * as defined by an expiration time value. 037 * </p> 038 * <p> 039 * When accessing the mapped value for a key, its expiration time is checked, 040 * and if it is a negative value or if it is greater than the current time, the 041 * mapped value is returned. Otherwise, the key is removed from the decorated 042 * map, and <code>null</code> is returned. 043 * </p> 044 * <p> 045 * When invoking methods that involve accessing the entire map contents (i.e 046 * {@link #containsKey(Object)}, {@link #entrySet()}, etc.) this decorator 047 * removes all expired entries prior to actually completing the invocation. 048 * </p> 049 * <p> 050 * <strong>Note that {@link PassiveExpiringMap} is not synchronized and is not 051 * thread-safe.</strong> If you wish to use this map from multiple threads 052 * concurrently, you must use appropriate synchronization. The simplest approach 053 * is to wrap this map using {@link java.util.Collections#synchronizedMap(Map)}. 054 * This class may throw exceptions when accessed by concurrent threads without 055 * synchronization. 056 * </p> 057 * 058 * @param <K> the type of the keys in this map 059 * @param <V> the type of the values in this map 060 * @since 4.0 061 */ 062public class PassiveExpiringMap<K, V> 063 extends AbstractMapDecorator<K, V> 064 implements Serializable { 065 066 /** 067 * A {@link org.apache.commons.collections4.map.PassiveExpiringMap.ExpirationPolicy ExpirationPolicy} 068 * that returns a expiration time that is a 069 * constant about of time in the future from the current time. 070 * 071 * @param <K> the type of the keys in the map 072 * @param <V> the type of the values in the map 073 * @since 4.0 074 */ 075 public static class ConstantTimeToLiveExpirationPolicy<K, V> 076 implements ExpirationPolicy<K, V> { 077 078 /** Serialization version */ 079 private static final long serialVersionUID = 1L; 080 081 /** the constant time-to-live value measured in milliseconds. */ 082 private final long timeToLiveMillis; 083 084 /** 085 * Default constructor. Constructs a policy using a negative 086 * time-to-live value that results in entries never expiring. 087 */ 088 public ConstantTimeToLiveExpirationPolicy() { 089 this(-1L); 090 } 091 092 /** 093 * Construct a policy with the given time-to-live constant measured in 094 * milliseconds. A negative time-to-live value indicates entries never 095 * expire. A zero time-to-live value indicates entries expire (nearly) 096 * immediately. 097 * 098 * @param timeToLiveMillis the constant amount of time (in milliseconds) 099 * an entry is available before it expires. A negative value 100 * results in entries that NEVER expire. A zero value results in 101 * entries that ALWAYS expire. 102 */ 103 public ConstantTimeToLiveExpirationPolicy(final long timeToLiveMillis) { 104 super(); 105 this.timeToLiveMillis = timeToLiveMillis; 106 } 107 108 /** 109 * Construct a policy with the given time-to-live constant measured in 110 * the given time unit of measure. 111 * 112 * @param timeToLive the constant amount of time an entry is available 113 * before it expires. A negative value results in entries that 114 * NEVER expire. A zero value results in entries that ALWAYS 115 * expire. 116 * @param timeUnit the unit of time for the <code>timeToLive</code> 117 * parameter, must not be null. 118 * @throws NullPointerException if the time unit is null. 119 */ 120 public ConstantTimeToLiveExpirationPolicy(final long timeToLive, 121 final TimeUnit timeUnit) { 122 this(validateAndConvertToMillis(timeToLive, timeUnit)); 123 } 124 125 /** 126 * Determine the expiration time for the given key-value entry. 127 * 128 * @param key the key for the entry (ignored). 129 * @param value the value for the entry (ignored). 130 * @return if {@link #timeToLiveMillis} ≥ 0, an expiration time of 131 * {@link #timeToLiveMillis} + 132 * {@link System#currentTimeMillis()} is returned. Otherwise, -1 133 * is returned indicating the entry never expires. 134 */ 135 @Override 136 public long expirationTime(final K key, final V value) { 137 if (timeToLiveMillis >= 0L) { 138 // avoid numerical overflow 139 final long now = System.currentTimeMillis(); 140 if (now > Long.MAX_VALUE - timeToLiveMillis) { 141 // expiration would be greater than Long.MAX_VALUE 142 // never expire 143 return -1; 144 } 145 146 // timeToLiveMillis in the future 147 return now + timeToLiveMillis; 148 } 149 150 // never expire 151 return -1L; 152 } 153 } 154 155 /** 156 * A policy to determine the expiration time for key-value entries. 157 * 158 * @param <K> the key object type. 159 * @param <V> the value object type 160 * @since 4.0 161 */ 162 public interface ExpirationPolicy<K, V> 163 extends Serializable { 164 165 /** 166 * Determine the expiration time for the given key-value entry. 167 * 168 * @param key the key for the entry. 169 * @param value the value for the entry. 170 * @return the expiration time value measured in milliseconds. A 171 * negative return value indicates the entry never expires. 172 */ 173 long expirationTime(K key, V value); 174 } 175 176 /** Serialization version */ 177 private static final long serialVersionUID = 1L; 178 179 /** 180 * First validate the input parameters. If the parameters are valid, convert 181 * the given time measured in the given units to the same time measured in 182 * milliseconds. 183 * 184 * @param timeToLive the constant amount of time an entry is available 185 * before it expires. A negative value results in entries that NEVER 186 * expire. A zero value results in entries that ALWAYS expire. 187 * @param timeUnit the unit of time for the <code>timeToLive</code> 188 * parameter, must not be null. 189 * @throws NullPointerException if the time unit is null. 190 */ 191 private static long validateAndConvertToMillis(final long timeToLive, 192 final TimeUnit timeUnit) { 193 if (timeUnit == null) { 194 throw new NullPointerException("Time unit must not be null"); 195 } 196 return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit); 197 } 198 199 /** map used to manage expiration times for the actual map entries. */ 200 private final Map<Object, Long> expirationMap = new HashMap<>(); 201 202 /** the policy used to determine time-to-live values for map entries. */ 203 private final ExpirationPolicy<K, V> expiringPolicy; 204 205 /** 206 * Default constructor. Constructs a map decorator that results in entries 207 * NEVER expiring. 208 */ 209 public PassiveExpiringMap() { 210 this(-1L); 211 } 212 213 /** 214 * Construct a map decorator using the given expiration policy to determine 215 * expiration times. 216 * 217 * @param expiringPolicy the policy used to determine expiration times of 218 * entries as they are added. 219 * @throws NullPointerException if expiringPolicy is null 220 */ 221 public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) { 222 this(expiringPolicy, new HashMap<K, V>()); 223 } 224 225 /** 226 * Construct a map decorator that decorates the given map and uses the given 227 * expiration policy to determine expiration times. If there are any 228 * elements already in the map being decorated, they will NEVER expire 229 * unless they are replaced. 230 * 231 * @param expiringPolicy the policy used to determine expiration times of 232 * entries as they are added. 233 * @param map the map to decorate, must not be null. 234 * @throws NullPointerException if the map or expiringPolicy is null. 235 */ 236 public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy, 237 final Map<K, V> map) { 238 super(map); 239 if (expiringPolicy == null) { 240 throw new NullPointerException("Policy must not be null."); 241 } 242 this.expiringPolicy = expiringPolicy; 243 } 244 245 /** 246 * Construct a map decorator that decorates the given map using the given 247 * time-to-live value measured in milliseconds to create and use a 248 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. 249 * 250 * @param timeToLiveMillis the constant amount of time (in milliseconds) an 251 * entry is available before it expires. A negative value results in 252 * entries that NEVER expire. A zero value results in entries that 253 * ALWAYS expire. 254 */ 255 public PassiveExpiringMap(final long timeToLiveMillis) { 256 this(new ConstantTimeToLiveExpirationPolicy<K, V>(timeToLiveMillis), 257 new HashMap<K, V>()); 258 } 259 260 /** 261 * Construct a map decorator using the given time-to-live value measured in 262 * milliseconds to create and use a 263 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there 264 * are any elements already in the map being decorated, they will NEVER 265 * expire unless they are replaced. 266 * 267 * @param timeToLiveMillis the constant amount of time (in milliseconds) an 268 * entry is available before it expires. A negative value results in 269 * entries that NEVER expire. A zero value results in entries that 270 * ALWAYS expire. 271 * @param map the map to decorate, must not be null. 272 * @throws NullPointerException if the map is null. 273 */ 274 public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V> map) { 275 this(new ConstantTimeToLiveExpirationPolicy<K, V>(timeToLiveMillis), 276 map); 277 } 278 279 /** 280 * Construct a map decorator using the given time-to-live value measured in 281 * the given time units of measure to create and use a 282 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. 283 * 284 * @param timeToLive the constant amount of time an entry is available 285 * before it expires. A negative value results in entries that NEVER 286 * expire. A zero value results in entries that ALWAYS expire. 287 * @param timeUnit the unit of time for the <code>timeToLive</code> 288 * parameter, must not be null. 289 * @throws NullPointerException if the time unit is null. 290 */ 291 public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) { 292 this(validateAndConvertToMillis(timeToLive, timeUnit)); 293 } 294 295 /** 296 * Construct a map decorator that decorates the given map using the given 297 * time-to-live value measured in the given time units of measure to create 298 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This policy 299 * is used to determine expiration times. If there are any elements already 300 * in the map being decorated, they will NEVER expire unless they are 301 * replaced. 302 * 303 * @param timeToLive the constant amount of time an entry is available 304 * before it expires. A negative value results in entries that NEVER 305 * expire. A zero value results in entries that ALWAYS expire. 306 * @param timeUnit the unit of time for the <code>timeToLive</code> 307 * parameter, must not be null. 308 * @param map the map to decorate, must not be null. 309 * @throws NullPointerException if the map or time unit is null. 310 */ 311 public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit, final Map<K, V> map) { 312 this(validateAndConvertToMillis(timeToLive, timeUnit), map); 313 } 314 315 /** 316 * Constructs a map decorator that decorates the given map and results in 317 * entries NEVER expiring. If there are any elements already in the map 318 * being decorated, they also will NEVER expire. 319 * 320 * @param map the map to decorate, must not be null. 321 * @throws NullPointerException if the map is null. 322 */ 323 public PassiveExpiringMap(final Map<K, V> map) { 324 this(-1L, map); 325 } 326 327 /** 328 * Normal {@link Map#clear()} behavior with the addition of clearing all 329 * expiration entries as well. 330 */ 331 @Override 332 public void clear() { 333 super.clear(); 334 expirationMap.clear(); 335 } 336 337 /** 338 * All expired entries are removed from the map prior to determining the 339 * contains result. 340 * {@inheritDoc} 341 */ 342 @Override 343 public boolean containsKey(final Object key) { 344 removeIfExpired(key, now()); 345 return super.containsKey(key); 346 } 347 348 /** 349 * All expired entries are removed from the map prior to determining the 350 * contains result. 351 * {@inheritDoc} 352 */ 353 @Override 354 public boolean containsValue(final Object value) { 355 removeAllExpired(now()); 356 return super.containsValue(value); 357 } 358 359 /** 360 * All expired entries are removed from the map prior to returning the entry set. 361 * {@inheritDoc} 362 */ 363 @Override 364 public Set<Entry<K, V>> entrySet() { 365 removeAllExpired(now()); 366 return super.entrySet(); 367 } 368 369 /** 370 * All expired entries are removed from the map prior to returning the entry value. 371 * {@inheritDoc} 372 */ 373 @Override 374 public V get(final Object key) { 375 removeIfExpired(key, now()); 376 return super.get(key); 377 } 378 379 /** 380 * All expired entries are removed from the map prior to determining if it is empty. 381 * {@inheritDoc} 382 */ 383 @Override 384 public boolean isEmpty() { 385 removeAllExpired(now()); 386 return super.isEmpty(); 387 } 388 389 /** 390 * Determines if the given expiration time is less than <code>now</code>. 391 * 392 * @param now the time in milliseconds used to compare against the 393 * expiration time. 394 * @param expirationTimeObject the expiration time value retrieved from 395 * {@link #expirationMap}, can be null. 396 * @return <code>true</code> if <code>expirationTimeObject</code> is ≥ 0 397 * and <code>expirationTimeObject</code> < <code>now</code>. 398 * <code>false</code> otherwise. 399 */ 400 private boolean isExpired(final long now, final Long expirationTimeObject) { 401 if (expirationTimeObject != null) { 402 final long expirationTime = expirationTimeObject.longValue(); 403 return expirationTime >= 0 && now >= expirationTime; 404 } 405 return false; 406 } 407 408 /** 409 * All expired entries are removed from the map prior to returning the key set. 410 * {@inheritDoc} 411 */ 412 @Override 413 public Set<K> keySet() { 414 removeAllExpired(now()); 415 return super.keySet(); 416 } 417 418 /** 419 * The current time in milliseconds. 420 */ 421 private long now() { 422 return System.currentTimeMillis(); 423 } 424 425 /** 426 * Add the given key-value pair to this map as well as recording the entry's expiration time based on 427 * the current time in milliseconds and this map's {@link #expiringPolicy}. 428 * <p> 429 * {@inheritDoc} 430 */ 431 @Override 432 public V put(final K key, final V value) { 433 // record expiration time of new entry 434 final long expirationTime = expiringPolicy.expirationTime(key, value); 435 expirationMap.put(key, Long.valueOf(expirationTime)); 436 437 return super.put(key, value); 438 } 439 440 @Override 441 public void putAll(final Map<? extends K, ? extends V> mapToCopy) { 442 for (final Map.Entry<? extends K, ? extends V> entry : mapToCopy.entrySet()) { 443 put(entry.getKey(), entry.getValue()); 444 } 445 } 446 447 /** 448 * Normal {@link Map#remove(Object)} behavior with the addition of removing 449 * any expiration entry as well. 450 * {@inheritDoc} 451 */ 452 @Override 453 public V remove(final Object key) { 454 expirationMap.remove(key); 455 return super.remove(key); 456 } 457 458 /** 459 * Removes all entries in the map whose expiration time is less than 460 * <code>now</code>. The exceptions are entries with negative expiration 461 * times; those entries are never removed. 462 * 463 * @see #isExpired(long, Long) 464 */ 465 private void removeAllExpired(final long now) { 466 final Iterator<Map.Entry<Object, Long>> iter = expirationMap.entrySet().iterator(); 467 while (iter.hasNext()) { 468 final Map.Entry<Object, Long> expirationEntry = iter.next(); 469 if (isExpired(now, expirationEntry.getValue())) { 470 // remove entry from collection 471 super.remove(expirationEntry.getKey()); 472 // remove entry from expiration map 473 iter.remove(); 474 } 475 } 476 } 477 478 /** 479 * Removes the entry with the given key if the entry's expiration time is 480 * less than <code>now</code>. If the entry has a negative expiration time, 481 * the entry is never removed. 482 */ 483 private void removeIfExpired(final Object key, final long now) { 484 final Long expirationTimeObject = expirationMap.get(key); 485 if (isExpired(now, expirationTimeObject)) { 486 remove(key); 487 } 488 } 489 490 /** 491 * All expired entries are removed from the map prior to returning the size. 492 * {@inheritDoc} 493 */ 494 @Override 495 public int size() { 496 removeAllExpired(now()); 497 return super.size(); 498 } 499 500 /** 501 * Read the map in using a custom routine. 502 * 503 * @param in the input stream 504 * @throws IOException if an error occurs while reading from the stream 505 * @throws ClassNotFoundException if an object read from the stream can not be loaded 506 */ 507 @SuppressWarnings("unchecked") 508 // (1) should only fail if input stream is incorrect 509 private void readObject(final ObjectInputStream in) 510 throws IOException, ClassNotFoundException { 511 in.defaultReadObject(); 512 map = (Map<K, V>) in.readObject(); // (1) 513 } 514 515 /** 516 * Write the map out using a custom routine. 517 * 518 * @param out the output stream 519 * @throws IOException if an error occurs while writing to the stream 520 */ 521 private void writeObject(final ObjectOutputStream out) 522 throws IOException { 523 out.defaultWriteObject(); 524 out.writeObject(map); 525 } 526 527 /** 528 * All expired entries are removed from the map prior to returning the value collection. 529 * {@inheritDoc} 530 */ 531 @Override 532 public Collection<V> values() { 533 removeAllExpired(now()); 534 return super.values(); 535 } 536}