1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.collections4.map;
18
19 import java.io.IOException;
20 import java.io.ObjectInputStream;
21 import java.io.ObjectOutputStream;
22 import java.io.Serializable;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.Iterator;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Set;
29 import java.util.concurrent.TimeUnit;
30
31 /**
32 * Decorates a {@code Map} to evict expired entries once their expiration
33 * time has been reached.
34 * <p>
35 * When putting a key-value pair in the map this decorator uses a
36 * {@link ExpirationPolicy} to determine how long the entry should remain alive
37 * as defined by an expiration time value.
38 * </p>
39 * <p>
40 * When accessing the mapped value for a key, its expiration time is checked,
41 * and if it is a negative value or if it is greater than the current time, the
42 * mapped value is returned. Otherwise, the key is removed from the decorated
43 * map, and {@code null} is returned.
44 * </p>
45 * <p>
46 * When invoking methods that involve accessing the entire map contents (i.e
47 * {@link #containsValue(Object)}, {@link #entrySet()}, etc.) this decorator
48 * removes all expired entries prior to actually completing the invocation.
49 * </p>
50 * <p>
51 * <strong>Note that {@link PassiveExpiringMap} is not synchronized and is not
52 * thread-safe.</strong> If you wish to use this map from multiple threads
53 * concurrently, you must use appropriate synchronization. The simplest approach
54 * is to wrap this map using {@link java.util.Collections#synchronizedMap(Map)}.
55 * This class may throw exceptions when accessed by concurrent threads without
56 * synchronization.
57 * </p>
58 *
59 * @param <K> the type of the keys in this map
60 * @param <V> the type of the values in this map
61 * @since 4.0
62 */
63 public class PassiveExpiringMap<K, V>
64 extends AbstractMapDecorator<K, V>
65 implements Serializable {
66
67 /**
68 * A {@link org.apache.commons.collections4.map.PassiveExpiringMap.ExpirationPolicy ExpirationPolicy}
69 * that returns an expiration time that is a
70 * constant about of time in the future from the current time.
71 *
72 * @param <K> the type of the keys in the map
73 * @param <V> the type of the values in the map
74 * @since 4.0
75 */
76 public static class ConstantTimeToLiveExpirationPolicy<K, V>
77 implements ExpirationPolicy<K, V> {
78
79 /** Serialization version */
80 private static final long serialVersionUID = 1L;
81
82 /** The constant time-to-live value measured in milliseconds. */
83 private final long timeToLiveMillis;
84
85 /**
86 * Default constructor. Constructs a policy using a negative
87 * time-to-live value that results in entries never expiring.
88 */
89 public ConstantTimeToLiveExpirationPolicy() {
90 this(-1L);
91 }
92
93 /**
94 * Constructs a policy with the given time-to-live constant measured in
95 * milliseconds. A negative time-to-live value indicates entries never
96 * expire. A zero time-to-live value indicates entries expire (nearly)
97 * immediately.
98 *
99 * @param timeToLiveMillis the constant amount of time (in milliseconds)
100 * an entry is available before it expires. A negative value
101 * results in entries that NEVER expire. A zero value results in
102 * entries that ALWAYS expire.
103 */
104 public ConstantTimeToLiveExpirationPolicy(final long timeToLiveMillis) {
105 this.timeToLiveMillis = timeToLiveMillis;
106 }
107
108 /**
109 * Constructs 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}
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 nowMillis = System.currentTimeMillis();
140 if (nowMillis > 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 nowMillis + 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 @FunctionalInterface
163 public interface ExpirationPolicy<K, V>
164 extends Serializable {
165
166 /**
167 * Determine the expiration time for the given key-value entry.
168 *
169 * @param key the key for the entry.
170 * @param value the value for the entry.
171 * @return the expiration time value measured in milliseconds. A
172 * negative return value indicates the entry never expires.
173 */
174 long expirationTime(K key, V value);
175 }
176
177 /** Serialization version */
178 private static final long serialVersionUID = 1L;
179
180 /**
181 * First validate the input parameters. If the parameters are valid, convert
182 * the given time measured in the given units to the same time measured in
183 * milliseconds.
184 *
185 * @param timeToLive the constant amount of time an entry is available
186 * before it expires. A negative value results in entries that NEVER
187 * expire. A zero value results in entries that ALWAYS expire.
188 * @param timeUnit the unit of time for the {@code timeToLive}
189 * parameter, must not be null.
190 * @throws NullPointerException if the time unit is null.
191 */
192 private static long validateAndConvertToMillis(final long timeToLive,
193 final TimeUnit timeUnit) {
194 Objects.requireNonNull(timeUnit, "timeUnit");
195 return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit);
196 }
197
198 /** Map used to manage expiration times for the actual map entries. */
199 private final Map<Object, Long> expirationMap = new HashMap<>();
200
201 /** The policy used to determine time-to-live values for map entries. */
202 private final ExpirationPolicy<K, V> expiringPolicy;
203
204 /**
205 * Default constructor. Constructs a map decorator that results in entries
206 * NEVER expiring.
207 */
208 public PassiveExpiringMap() {
209 this(-1L);
210 }
211
212 /**
213 * Constructs a map decorator using the given expiration policy to determine
214 * expiration times.
215 *
216 * @param expiringPolicy the policy used to determine expiration times of
217 * entries as they are added.
218 * @throws NullPointerException if expiringPolicy is null
219 */
220 public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) {
221 this(expiringPolicy, new HashMap<>());
222 }
223
224 /**
225 * Constructs a map decorator that decorates the given map and uses the given
226 * expiration policy to determine expiration times. If there are any
227 * elements already in the map being decorated, they will NEVER expire
228 * unless they are replaced.
229 *
230 * @param expiringPolicy the policy used to determine expiration times of
231 * entries as they are added.
232 * @param map the map to decorate, must not be null.
233 * @throws NullPointerException if the map or expiringPolicy is null.
234 */
235 public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy,
236 final Map<K, V> map) {
237 super(map);
238 this.expiringPolicy = Objects.requireNonNull(expiringPolicy, "expiringPolicy");
239 }
240
241 /**
242 * Constructs a map decorator that decorates the given map using the given
243 * time-to-live value measured in milliseconds to create and use a
244 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
245 *
246 * @param timeToLiveMillis the constant amount of time (in milliseconds) an
247 * entry is available before it expires. A negative value results in
248 * entries that NEVER expire. A zero value results in entries that
249 * ALWAYS expire.
250 */
251 public PassiveExpiringMap(final long timeToLiveMillis) {
252 this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
253 new HashMap<>());
254 }
255
256 /**
257 * Constructs a map decorator using the given time-to-live value measured in
258 * milliseconds to create and use a
259 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there
260 * are any elements already in the map being decorated, they will NEVER
261 * expire unless they are replaced.
262 *
263 * @param timeToLiveMillis the constant amount of time (in milliseconds) an
264 * entry is available before it expires. A negative value results in
265 * entries that NEVER expire. A zero value results in entries that
266 * ALWAYS expire.
267 * @param map the map to decorate, must not be null.
268 * @throws NullPointerException if the map is null.
269 */
270 public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V> map) {
271 this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
272 map);
273 }
274
275 /**
276 * Constructs a map decorator using the given time-to-live value measured in
277 * the given time units of measure to create and use a
278 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
279 *
280 * @param timeToLive the constant amount of time an entry is available
281 * before it expires. A negative value results in entries that NEVER
282 * expire. A zero value results in entries that ALWAYS expire.
283 * @param timeUnit the unit of time for the {@code timeToLive}
284 * parameter, must not be null.
285 * @throws NullPointerException if the time unit is null.
286 */
287 public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) {
288 this(validateAndConvertToMillis(timeToLive, timeUnit));
289 }
290
291 /**
292 * Constructs a map decorator that decorates the given map using the given
293 * time-to-live value measured in the given time units of measure to create
294 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This policy
295 * is used to determine expiration times. If there are any elements already
296 * in the map being decorated, they will NEVER expire unless they are
297 * replaced.
298 *
299 * @param timeToLive the constant amount of time an entry is available
300 * before it expires. A negative value results in entries that NEVER
301 * expire. A zero value results in entries that ALWAYS expire.
302 * @param timeUnit the unit of time for the {@code timeToLive}
303 * parameter, must not be null.
304 * @param map the map to decorate, must not be null.
305 * @throws NullPointerException if the map or time unit is null.
306 */
307 public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit, final Map<K, V> map) {
308 this(validateAndConvertToMillis(timeToLive, timeUnit), map);
309 }
310
311 /**
312 * Constructs a map decorator that decorates the given map and results in
313 * entries NEVER expiring. If there are any elements already in the map
314 * being decorated, they also will NEVER expire.
315 *
316 * @param map the map to decorate, must not be null.
317 * @throws NullPointerException if the map is null.
318 */
319 public PassiveExpiringMap(final Map<K, V> map) {
320 this(-1L, map);
321 }
322
323 /**
324 * Normal {@link Map#clear()} behavior with the addition of clearing all
325 * expiration entries as well.
326 */
327 @Override
328 public void clear() {
329 super.clear();
330 expirationMap.clear();
331 }
332
333 /**
334 * All expired entries are removed from the map prior to determining the
335 * contains result.
336 * {@inheritDoc}
337 */
338 @Override
339 public boolean containsKey(final Object key) {
340 removeIfExpired(key, now());
341 return super.containsKey(key);
342 }
343
344 /**
345 * All expired entries are removed from the map prior to determining the
346 * contains result.
347 * {@inheritDoc}
348 */
349 @Override
350 public boolean containsValue(final Object value) {
351 removeAllExpired(now());
352 return super.containsValue(value);
353 }
354
355 /**
356 * All expired entries are removed from the map prior to returning the entry set.
357 * {@inheritDoc}
358 */
359 @Override
360 public Set<Entry<K, V>> entrySet() {
361 removeAllExpired(now());
362 return super.entrySet();
363 }
364
365 /**
366 * All expired entries are removed from the map prior to returning the entry value.
367 * {@inheritDoc}
368 */
369 @Override
370 public V get(final Object key) {
371 removeIfExpired(key, now());
372 return super.get(key);
373 }
374
375 /**
376 * All expired entries are removed from the map prior to determining if it is empty.
377 * {@inheritDoc}
378 */
379 @Override
380 public boolean isEmpty() {
381 removeAllExpired(now());
382 return super.isEmpty();
383 }
384
385 /**
386 * Determines if the given expiration time is less than {@code now}.
387 *
388 * @param now the time in milliseconds used to compare against the
389 * expiration time.
390 * @param expirationTimeObject the expiration time value retrieved from
391 * {@link #expirationMap}, can be null.
392 * @return {@code true} if {@code expirationTimeObject} is ≥ 0
393 * and {@code expirationTimeObject} < {@code now}.
394 * {@code false} otherwise.
395 */
396 private boolean isExpired(final long now, final Long expirationTimeObject) {
397 if (expirationTimeObject != null) {
398 final long expirationTime = expirationTimeObject.longValue();
399 return expirationTime >= 0 && now >= expirationTime;
400 }
401 return false;
402 }
403
404 /**
405 * All expired entries are removed from the map prior to returning the key set.
406 * {@inheritDoc}
407 */
408 @Override
409 public Set<K> keySet() {
410 removeAllExpired(now());
411 return super.keySet();
412 }
413
414 /**
415 * The current time in milliseconds.
416 */
417 private long now() {
418 return System.currentTimeMillis();
419 }
420
421 /**
422 * {@inheritDoc}
423 * <p>
424 * Add the given key-value pair to this map as well as recording the entry's expiration time based on the current time in milliseconds and this map's
425 * {@link #expiringPolicy}.
426 * </p>
427 */
428 @Override
429 public V put(final K key, final V value) {
430 // remove the previous record
431 removeIfExpired(key, now());
432
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 * Deserializes the map in using a custom routine.
449 *
450 * @param in the input stream
451 * @throws IOException if an error occurs while reading from the stream
452 * @throws ClassNotFoundException if an object read from the stream cannot be loaded
453 */
454 @SuppressWarnings("unchecked")
455 // (1) should only fail if input stream is incorrect
456 private void readObject(final ObjectInputStream in)
457 throws IOException, ClassNotFoundException {
458 in.defaultReadObject();
459 map = (Map<K, V>) in.readObject(); // (1)
460 }
461
462 /**
463 * Normal {@link Map#remove(Object)} behavior with the addition of removing
464 * any expiration entry as well.
465 * {@inheritDoc}
466 */
467 @Override
468 public V remove(final Object key) {
469 expirationMap.remove(key);
470 return super.remove(key);
471 }
472
473 /**
474 * Removes all entries in the map whose expiration time is less than
475 * {@code now}. The exceptions are entries with negative expiration
476 * times; those entries are never removed.
477 *
478 * @see #isExpired(long, Long)
479 */
480 private void removeAllExpired(final long nowMillis) {
481 final Iterator<Map.Entry<Object, Long>> iter = expirationMap.entrySet().iterator();
482 while (iter.hasNext()) {
483 final Map.Entry<Object, Long> expirationEntry = iter.next();
484 if (isExpired(nowMillis, expirationEntry.getValue())) {
485 // remove entry from collection
486 super.remove(expirationEntry.getKey());
487 // remove entry from expiration map
488 iter.remove();
489 }
490 }
491 }
492
493 /**
494 * Removes the entry with the given key if the entry's expiration time is
495 * less than {@code now}. If the entry has a negative expiration time,
496 * the entry is never removed.
497 */
498 private void removeIfExpired(final Object key, final long nowMillis) {
499 final Long expirationTimeObject = expirationMap.get(key);
500 if (isExpired(nowMillis, expirationTimeObject)) {
501 remove(key);
502 }
503 }
504
505 /**
506 * All expired entries are removed from the map prior to returning the size.
507 * {@inheritDoc}
508 */
509 @Override
510 public int size() {
511 removeAllExpired(now());
512 return super.size();
513 }
514
515 /**
516 * All expired entries are removed from the map prior to returning the value collection.
517 * {@inheritDoc}
518 */
519 @Override
520 public Collection<V> values() {
521 removeAllExpired(now());
522 return super.values();
523 }
524
525 /**
526 * Serializes this object to an ObjectOutputStream.
527 *
528 * @param out the target ObjectOutputStream.
529 * @throws IOException thrown when an I/O errors occur writing to the target stream.
530 */
531 private void writeObject(final ObjectOutputStream out)
532 throws IOException {
533 out.defaultWriteObject();
534 out.writeObject(map);
535 }
536 }