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 * https://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 import java.util.function.Predicate;
31
32 import org.apache.commons.collections4.collection.AbstractCollectionDecorator;
33 import org.apache.commons.collections4.iterators.AbstractIteratorDecorator;
34 import org.apache.commons.collections4.set.AbstractSetDecorator;
35
36 /**
37 * Decorates a {@code Map} to evict expired entries once their expiration
38 * time has been reached.
39 * <p>
40 * When putting a key-value pair in the map this decorator uses a
41 * {@link ExpirationPolicy} to determine how long the entry should remain alive
42 * as defined by an expiration time value.
43 * </p>
44 * <p>
45 * When accessing the mapped value for a key, its expiration time is checked,
46 * and if it is a negative value or if it is greater than the current time, the
47 * mapped value is returned. Otherwise, the key is removed from the decorated
48 * map, and {@code null} is returned.
49 * </p>
50 * <p>
51 * When invoking methods that involve accessing the entire map contents (i.e
52 * {@link #containsValue(Object)}, {@link #entrySet()}, etc.) this decorator
53 * removes all expired entries prior to actually completing the invocation.
54 * </p>
55 * <p>
56 * <strong>Note that {@link PassiveExpiringMap} is not synchronized and is not
57 * thread-safe.</strong> If you wish to use this map from multiple threads
58 * concurrently, you must use appropriate synchronization. The simplest approach
59 * is to wrap this map using {@link java.util.Collections#synchronizedMap(Map)}.
60 * This class may throw exceptions when accessed by concurrent threads without
61 * synchronization.
62 * </p>
63 *
64 * @param <K> The type of the keys in this map
65 * @param <V> The type of the values in this map
66 * @since 4.0
67 */
68 public class PassiveExpiringMap<K, V>
69 extends AbstractMapDecorator<K, V>
70 implements Serializable {
71
72 /**
73 * A {@link ExpirationPolicy ExpirationPolicy}
74 * that returns an expiration time that is a
75 * constant about of time in the future from the current time.
76 *
77 * @param <K> The type of the keys in the map
78 * @param <V> The type of the values in the map
79 * @since 4.0
80 */
81 public static class ConstantTimeToLiveExpirationPolicy<K, V>
82 implements ExpirationPolicy<K, V> {
83
84 /** Serialization version */
85 private static final long serialVersionUID = 1L;
86
87 /** The constant time-to-live value measured in milliseconds. */
88 private final long timeToLiveMillis;
89
90 /**
91 * Default constructor. Constructs a policy using a negative
92 * time-to-live value that results in entries never expiring.
93 */
94 public ConstantTimeToLiveExpirationPolicy() {
95 this(-1L);
96 }
97
98 /**
99 * Constructs a policy with the given time-to-live constant measured in
100 * milliseconds. A negative time-to-live value indicates entries never
101 * expire. A zero time-to-live value indicates entries expire (nearly)
102 * immediately.
103 *
104 * @param timeToLiveMillis The constant amount of time (in milliseconds)
105 * an entry is available before it expires. A negative value
106 * results in entries that NEVER expire. A zero value results in
107 * entries that ALWAYS expire.
108 */
109 public ConstantTimeToLiveExpirationPolicy(final long timeToLiveMillis) {
110 this.timeToLiveMillis = timeToLiveMillis;
111 }
112
113 /**
114 * Constructs a policy with the given time-to-live constant measured in
115 * the given time unit of measure.
116 *
117 * @param timeToLive The constant amount of time an entry is available
118 * before it expires. A negative value results in entries that
119 * NEVER expire. A zero value results in entries that ALWAYS
120 * expire.
121 * @param timeUnit The unit of time for the {@code timeToLive}
122 * parameter, must not be null.
123 * @throws NullPointerException if the time unit is null.
124 */
125 public ConstantTimeToLiveExpirationPolicy(final long timeToLive,
126 final TimeUnit timeUnit) {
127 this(validateAndConvertToMillis(timeToLive, timeUnit));
128 }
129
130 /**
131 * Determine the expiration time for the given key-value entry.
132 *
133 * @param key The key for the entry (ignored).
134 * @param value The value for the entry (ignored).
135 * @return if {@link #timeToLiveMillis} ≥ 0, an expiration time of
136 * {@link #timeToLiveMillis} +
137 * {@link System#currentTimeMillis()} is returned. Otherwise, -1
138 * is returned indicating the entry never expires.
139 */
140 @Override
141 public long expirationTime(final K key, final V value) {
142 if (timeToLiveMillis >= 0L) {
143 // avoid numerical overflow
144 final long nowMillis = System.currentTimeMillis();
145 if (nowMillis > Long.MAX_VALUE - timeToLiveMillis) {
146 // expiration would be greater than Long.MAX_VALUE
147 // never expire
148 return -1;
149 }
150
151 // timeToLiveMillis in the future
152 return nowMillis + timeToLiveMillis;
153 }
154
155 // never expire
156 return -1L;
157 }
158 }
159
160 private final class EntrySet extends AbstractSetDecorator<Entry<K, V>> {
161
162 /** Generated serial version ID. */
163 private static final long serialVersionUID = 1L;
164
165 private EntrySet(final Set<Entry<K, V>> set) {
166 super(set);
167 }
168
169 @Override
170 public void clear() {
171 PassiveExpiringMap.this.clear();
172 }
173
174 @Override
175 public boolean contains(final Object object) {
176 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
177 return super.contains(object);
178 }
179
180 @Override
181 public boolean containsAll(final Collection<?> coll) {
182 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
183 return super.containsAll(coll);
184 }
185
186 @Override
187 public boolean isEmpty() {
188 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
189 return super.isEmpty();
190 }
191
192 @Override
193 public Iterator<Entry<K, V>> iterator() {
194 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
195 return new EntrySetIterator(super.iterator());
196 }
197
198 @Override
199 public boolean remove(final Object object) {
200 if (object instanceof Map.Entry) {
201 final Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
202 final Object key = entry.getKey();
203 if (PassiveExpiringMap.this.containsKey(key)) {
204 final Object value = PassiveExpiringMap.this.get(key);
205 if (Objects.equals(value, entry.getValue())) {
206 PassiveExpiringMap.this.remove(key);
207 return true;
208 }
209 }
210 }
211 return false;
212 }
213
214 @Override
215 public boolean removeAll(final Collection<?> coll) {
216 Objects.requireNonNull(coll, "coll");
217 boolean changed = false;
218 if (size() > coll.size()) {
219 for (final Object obj : coll) {
220 changed |= remove(obj);
221 }
222 } else {
223 final Iterator<?> it = iterator();
224 while (it.hasNext()) {
225 if (coll.contains(it.next())) {
226 it.remove();
227 changed = true;
228 }
229 }
230 }
231 return changed;
232 }
233
234 @Override
235 public boolean removeIf(final Predicate<? super Entry<K, V>> filter) {
236 Objects.requireNonNull(filter, "filter");
237 boolean changed = false;
238 final Iterator<Entry<K, V>> it = iterator();
239 while (it.hasNext()) {
240 if (filter.test(it.next())) {
241 it.remove();
242 changed = true;
243 }
244 }
245 return changed;
246 }
247
248 @Override
249 public boolean retainAll(final Collection<?> coll) {
250 Objects.requireNonNull(coll, "coll");
251 boolean changed = false;
252 final Iterator<?> it = iterator();
253 while (it.hasNext()) {
254 if (!coll.contains(it.next())) {
255 it.remove();
256 changed = true;
257 }
258 }
259 return changed;
260 }
261
262 @Override
263 public int size() {
264 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
265 return super.size();
266 }
267
268 @Override
269 public Object[] toArray() {
270 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
271 return super.toArray();
272 }
273
274 @Override
275 public <T> T[] toArray(final T[] array) {
276 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
277 return super.toArray(array);
278 }
279 }
280
281 private final class EntrySetIterator extends AbstractIteratorDecorator<Entry<K, V>> {
282 private Entry<K, V> lastReturned;
283
284 private EntrySetIterator(final Iterator<Entry<K, V>> iterator) {
285 super(iterator);
286 }
287
288 @Override
289 public Entry<K, V> next() {
290 lastReturned = super.next();
291 return lastReturned;
292 }
293
294 @Override
295 public void remove() {
296 super.remove();
297 if (lastReturned != null) {
298 PassiveExpiringMap.this.expirationMap.remove(lastReturned.getKey());
299 lastReturned = null;
300 }
301 }
302 }
303
304 /**
305 * A policy to determine the expiration time for key-value entries.
306 *
307 * @param <K> The key object type.
308 * @param <V> The value object type
309 * @since 4.0
310 */
311 @FunctionalInterface
312 public interface ExpirationPolicy<K, V>
313 extends Serializable {
314
315 /**
316 * Determine the expiration time for the given key-value entry.
317 *
318 * @param key The key for the entry.
319 * @param value The value for the entry.
320 * @return The expiration time value measured in milliseconds. A
321 * negative return value indicates the entry never expires.
322 */
323 long expirationTime(K key, V value);
324 }
325
326 private final class KeySet extends AbstractSetDecorator<K> {
327
328 /** Generated serial version ID. */
329 private static final long serialVersionUID = 1L;
330
331 private KeySet(final Set<K> set) {
332 super(set);
333 }
334
335 @Override
336 public void clear() {
337 PassiveExpiringMap.this.clear();
338 }
339
340 @Override
341 public boolean contains(final Object key) {
342 PassiveExpiringMap.this.removeIfExpired(key, PassiveExpiringMap.this.now());
343 return super.contains(key);
344 }
345
346 @Override
347 public boolean containsAll(final Collection<?> coll) {
348 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
349 return super.containsAll(coll);
350 }
351
352 @Override
353 public boolean isEmpty() {
354 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
355 return super.isEmpty();
356 }
357
358 @Override
359 public Iterator<K> iterator() {
360 return new KeySetIterator(PassiveExpiringMap.this.entrySet().iterator());
361 }
362
363 @Override
364 public boolean remove(final Object key) {
365 final boolean hasKey = contains(key);
366 if (hasKey) {
367 PassiveExpiringMap.this.remove(key);
368 }
369 return hasKey;
370 }
371
372 @Override
373 public boolean removeAll(final Collection<?> coll) {
374 Objects.requireNonNull(coll, "coll");
375 boolean changed = false;
376 if (size() > coll.size()) {
377 for (final Object obj : coll) {
378 changed |= remove(obj);
379 }
380 } else {
381 final Iterator<?> it = iterator();
382 while (it.hasNext()) {
383 if (coll.contains(it.next())) {
384 it.remove();
385 changed = true;
386 }
387 }
388 }
389 return changed;
390 }
391
392 @Override
393 public boolean removeIf(final Predicate<? super K> filter) {
394 Objects.requireNonNull(filter, "filter");
395 boolean changed = false;
396 final Iterator<K> it = iterator();
397 while (it.hasNext()) {
398 if (filter.test(it.next())) {
399 it.remove();
400 changed = true;
401 }
402 }
403 return changed;
404 }
405
406 @Override
407 public boolean retainAll(final Collection<?> coll) {
408 Objects.requireNonNull(coll, "coll");
409 boolean changed = false;
410 final Iterator<?> it = iterator();
411 while (it.hasNext()) {
412 if (!coll.contains(it.next())) {
413 it.remove();
414 changed = true;
415 }
416 }
417 return changed;
418 }
419
420 @Override
421 public int size() {
422 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
423 return super.size();
424 }
425
426 @Override
427 public Object[] toArray() {
428 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
429 return super.toArray();
430 }
431
432 @Override
433 public <T> T[] toArray(final T[] array) {
434 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
435 return super.toArray(array);
436 }
437 }
438
439 private final class KeySetIterator implements Iterator<K> {
440 private final Iterator<Entry<K, V>> iterator;
441
442 private KeySetIterator(final Iterator<Entry<K, V>> iterator) {
443 this.iterator = iterator;
444 }
445
446 @Override
447 public boolean hasNext() {
448 return iterator.hasNext();
449 }
450
451 @Override
452 public K next() {
453 return iterator.next().getKey();
454 }
455
456 @Override
457 public void remove() {
458 iterator.remove();
459 }
460 }
461
462 private final class ValuesCollection extends AbstractCollectionDecorator<V> {
463
464 /** Generated serial version ID. */
465 private static final long serialVersionUID = 1L;
466
467 private ValuesCollection(final Collection<V> coll) {
468 super(coll);
469 }
470
471 @Override
472 public void clear() {
473 PassiveExpiringMap.this.clear();
474 }
475
476 @Override
477 public boolean contains(final Object value) {
478 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
479 return super.contains(value);
480 }
481
482 @Override
483 public boolean containsAll(final Collection<?> coll) {
484 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
485 return super.containsAll(coll);
486 }
487
488 @Override
489 public boolean isEmpty() {
490 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
491 return super.isEmpty();
492 }
493
494 @Override
495 public Iterator<V> iterator() {
496 return new ValuesIterator(PassiveExpiringMap.this.entrySet().iterator());
497 }
498
499 @Override
500 public boolean remove(final Object value) {
501 final Iterator<V> it = iterator();
502 while (it.hasNext()) {
503 if (Objects.equals(it.next(), value)) {
504 it.remove();
505 return true;
506 }
507 }
508 return false;
509 }
510
511 @Override
512 public boolean removeAll(final Collection<?> coll) {
513 Objects.requireNonNull(coll, "coll");
514 boolean changed = false;
515 final Iterator<?> it = iterator();
516 while (it.hasNext()) {
517 if (coll.contains(it.next())) {
518 it.remove();
519 changed = true;
520 }
521 }
522 return changed;
523 }
524
525 @Override
526 public boolean removeIf(final Predicate<? super V> filter) {
527 Objects.requireNonNull(filter, "filter");
528 boolean changed = false;
529 final Iterator<V> it = iterator();
530 while (it.hasNext()) {
531 if (filter.test(it.next())) {
532 it.remove();
533 changed = true;
534 }
535 }
536 return changed;
537 }
538
539 @Override
540 public boolean retainAll(final Collection<?> coll) {
541 Objects.requireNonNull(coll, "coll");
542 boolean changed = false;
543 final Iterator<?> it = iterator();
544 while (it.hasNext()) {
545 if (!coll.contains(it.next())) {
546 it.remove();
547 changed = true;
548 }
549 }
550 return changed;
551 }
552
553 @Override
554 public int size() {
555 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
556 return super.size();
557 }
558
559 @Override
560 public Object[] toArray() {
561 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
562 return super.toArray();
563 }
564
565 @Override
566 public <T> T[] toArray(final T[] array) {
567 PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
568 return super.toArray(array);
569 }
570 }
571
572 private final class ValuesIterator implements Iterator<V> {
573 private final Iterator<Entry<K, V>> iterator;
574
575 private ValuesIterator(final Iterator<Entry<K, V>> iterator) {
576 this.iterator = iterator;
577 }
578
579 @Override
580 public boolean hasNext() {
581 return iterator.hasNext();
582 }
583
584 @Override
585 public V next() {
586 return iterator.next().getValue();
587 }
588
589 @Override
590 public void remove() {
591 iterator.remove();
592 }
593 }
594
595 /** Serialization version */
596 private static final long serialVersionUID = 1L;
597
598 /**
599 * First validate the input parameters. If the parameters are valid, convert
600 * the given time measured in the given units to the same time measured in
601 * milliseconds.
602 *
603 * @param timeToLive The constant amount of time an entry is available
604 * before it expires. A negative value results in entries that NEVER
605 * expire. A zero value results in entries that ALWAYS expire.
606 * @param timeUnit The unit of time for the {@code timeToLive}
607 * parameter, must not be null.
608 * @throws NullPointerException if the time unit is null.
609 */
610 private static long validateAndConvertToMillis(final long timeToLive,
611 final TimeUnit timeUnit) {
612 Objects.requireNonNull(timeUnit, "timeUnit");
613 return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit);
614 }
615
616 /** Map used to manage expiration times for the actual map entries. */
617 private final Map<Object, Long> expirationMap = new HashMap<>();
618
619 /** The policy used to determine time-to-live values for map entries. */
620 private final ExpirationPolicy<K, V> expiringPolicy;
621
622 /**
623 * Default constructor. Constructs a map decorator that results in entries
624 * NEVER expiring.
625 */
626 public PassiveExpiringMap() {
627 this(-1L);
628 }
629
630 /**
631 * Constructs a map decorator using the given expiration policy to determine
632 * expiration times.
633 *
634 * @param expiringPolicy The policy used to determine expiration times of
635 * entries as they are added.
636 * @throws NullPointerException if expiringPolicy is null
637 */
638 public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) {
639 this(expiringPolicy, new HashMap<>());
640 }
641
642 /**
643 * Constructs a map decorator that decorates the given map and uses the given
644 * expiration policy to determine expiration times. If there are any
645 * elements already in the map being decorated, they will NEVER expire
646 * unless they are replaced.
647 *
648 * @param expiringPolicy The policy used to determine expiration times of
649 * entries as they are added.
650 * @param map The map to decorate, must not be null.
651 * @throws NullPointerException if the map or expiringPolicy is null.
652 */
653 public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy,
654 final Map<K, V> map) {
655 super(map);
656 this.expiringPolicy = Objects.requireNonNull(expiringPolicy, "expiringPolicy");
657 }
658
659 /**
660 * Constructs a map decorator that decorates the given map using the given
661 * time-to-live value measured in milliseconds to create and use a
662 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
663 *
664 * @param timeToLiveMillis The constant amount of time (in milliseconds) an
665 * entry is available before it expires. A negative value results in
666 * entries that NEVER expire. A zero value results in entries that
667 * ALWAYS expire.
668 */
669 public PassiveExpiringMap(final long timeToLiveMillis) {
670 this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
671 new HashMap<>());
672 }
673
674 /**
675 * Constructs a map decorator using the given time-to-live value measured in
676 * milliseconds to create and use a
677 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there
678 * are any elements already in the map being decorated, they will NEVER
679 * expire unless they are replaced.
680 *
681 * @param timeToLiveMillis The constant amount of time (in milliseconds) an
682 * entry is available before it expires. A negative value results in
683 * entries that NEVER expire. A zero value results in entries that
684 * ALWAYS expire.
685 * @param map The map to decorate, must not be null.
686 * @throws NullPointerException if the map is null.
687 */
688 public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V> map) {
689 this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
690 map);
691 }
692
693 /**
694 * Constructs a map decorator using the given time-to-live value measured in
695 * the given time units of measure to create and use a
696 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
697 *
698 * @param timeToLive The constant amount of time an entry is available
699 * before it expires. A negative value results in entries that NEVER
700 * expire. A zero value results in entries that ALWAYS expire.
701 * @param timeUnit The unit of time for the {@code timeToLive}
702 * parameter, must not be null.
703 * @throws NullPointerException if the time unit is null.
704 */
705 public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) {
706 this(validateAndConvertToMillis(timeToLive, timeUnit));
707 }
708
709 /**
710 * Constructs a map decorator that decorates the given map using the given
711 * time-to-live value measured in the given time units of measure to create
712 * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This policy
713 * is used to determine expiration times. If there are any elements already
714 * in the map being decorated, they will NEVER expire unless they are
715 * replaced.
716 *
717 * @param timeToLive The constant amount of time an entry is available
718 * before it expires. A negative value results in entries that NEVER
719 * expire. A zero value results in entries that ALWAYS expire.
720 * @param timeUnit The unit of time for the {@code timeToLive}
721 * parameter, must not be null.
722 * @param map The map to decorate, must not be null.
723 * @throws NullPointerException if the map or time unit is null.
724 */
725 public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit, final Map<K, V> map) {
726 this(validateAndConvertToMillis(timeToLive, timeUnit), map);
727 }
728
729 /**
730 * Constructs a map decorator that decorates the given map and results in
731 * entries NEVER expiring. If there are any elements already in the map
732 * being decorated, they also will NEVER expire.
733 *
734 * @param map The map to decorate, must not be null.
735 * @throws NullPointerException if the map is null.
736 */
737 public PassiveExpiringMap(final Map<K, V> map) {
738 this(-1L, map);
739 }
740
741 /**
742 * Normal {@link Map#clear()} behavior with the addition of clearing all
743 * expiration entries as well.
744 */
745 @Override
746 public void clear() {
747 super.clear();
748 expirationMap.clear();
749 }
750
751 /**
752 * All expired entries are removed from the map prior to determining the
753 * contains result.
754 * {@inheritDoc}
755 */
756 @Override
757 public boolean containsKey(final Object key) {
758 removeIfExpired(key, now());
759 return super.containsKey(key);
760 }
761
762 /**
763 * All expired entries are removed from the map prior to determining the
764 * contains result.
765 * {@inheritDoc}
766 */
767 @Override
768 public boolean containsValue(final Object value) {
769 removeAllExpired(now());
770 return super.containsValue(value);
771 }
772
773 /**
774 * All expired entries are removed from the map prior to returning the entry set.
775 * {@inheritDoc}
776 */
777 @Override
778 public Set<Entry<K, V>> entrySet() {
779 removeAllExpired(now());
780 return new EntrySet(super.entrySet());
781 }
782
783 /**
784 * All expired entries are removed from the map prior to returning the entry value.
785 * {@inheritDoc}
786 */
787 @Override
788 public V get(final Object key) {
789 removeIfExpired(key, now());
790 return super.get(key);
791 }
792
793 /**
794 * All expired entries are removed from the map prior to determining if it is empty.
795 * {@inheritDoc}
796 */
797 @Override
798 public boolean isEmpty() {
799 removeAllExpired(now());
800 return super.isEmpty();
801 }
802
803 /**
804 * Determines if the given expiration time is less than {@code now}.
805 *
806 * @param now The time in milliseconds used to compare against the
807 * expiration time.
808 * @param expirationTimeObject The expiration time value retrieved from
809 * {@link #expirationMap}, can be null.
810 * @return {@code true} if {@code expirationTimeObject} is ≥ 0
811 * and {@code expirationTimeObject} < {@code now}.
812 * {@code false} otherwise.
813 */
814 private boolean isExpired(final long now, final Long expirationTimeObject) {
815 if (expirationTimeObject != null) {
816 final long expirationTime = expirationTimeObject.longValue();
817 return expirationTime >= 0 && now >= expirationTime;
818 }
819 return false;
820 }
821
822 /**
823 * All expired entries are removed from the map prior to returning the key set.
824 * {@inheritDoc}
825 */
826 @Override
827 public Set<K> keySet() {
828 removeAllExpired(now());
829 return new KeySet(super.keySet());
830 }
831
832 /**
833 * The current time in milliseconds.
834 */
835 private long now() {
836 return System.currentTimeMillis();
837 }
838
839 /**
840 * {@inheritDoc}
841 * <p>
842 * 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
843 * {@link #expiringPolicy}.
844 * </p>
845 */
846 @Override
847 public V put(final K key, final V value) {
848 // remove the previous record
849 removeIfExpired(key, now());
850
851 // record expiration time of new entry
852 final long expirationTime = expiringPolicy.expirationTime(key, value);
853 expirationMap.put(key, Long.valueOf(expirationTime));
854
855 return super.put(key, value);
856 }
857
858 @Override
859 public void putAll(final Map<? extends K, ? extends V> mapToCopy) {
860 for (final Map.Entry<? extends K, ? extends V> entry : mapToCopy.entrySet()) {
861 put(entry.getKey(), entry.getValue());
862 }
863 }
864
865 /**
866 * Deserializes the map in using a custom routine.
867 *
868 * @param in The input stream
869 * @throws IOException Thrown if an error occurs while reading from the stream
870 * @throws ClassNotFoundException if an object read from the stream cannot be loaded
871 */
872 @SuppressWarnings("unchecked")
873 // (1) should only fail if input stream is incorrect
874 private void readObject(final ObjectInputStream in)
875 throws IOException, ClassNotFoundException {
876 in.defaultReadObject();
877 map = (Map<K, V>) in.readObject(); // (1)
878 }
879
880 /**
881 * Normal {@link Map#remove(Object)} behavior with the addition of removing
882 * any expiration entry as well.
883 * {@inheritDoc}
884 */
885 @Override
886 public V remove(final Object key) {
887 expirationMap.remove(key);
888 return super.remove(key);
889 }
890
891 /**
892 * Removes all entries in the map whose expiration time is less than
893 * {@code now}. The exceptions are entries with negative expiration
894 * times; those entries are never removed.
895 *
896 * @see #isExpired(long, Long)
897 */
898 private void removeAllExpired(final long nowMillis) {
899 final Iterator<Map.Entry<Object, Long>> iter = expirationMap.entrySet().iterator();
900 while (iter.hasNext()) {
901 final Map.Entry<Object, Long> expirationEntry = iter.next();
902 if (isExpired(nowMillis, expirationEntry.getValue())) {
903 // remove entry from collection
904 super.remove(expirationEntry.getKey());
905 // remove entry from expiration map
906 iter.remove();
907 }
908 }
909 }
910
911 /**
912 * Removes the entry with the given key if the entry's expiration time is
913 * less than {@code now}. If the entry has a negative expiration time,
914 * the entry is never removed.
915 */
916 private void removeIfExpired(final Object key, final long nowMillis) {
917 final Long expirationTimeObject = expirationMap.get(key);
918 if (isExpired(nowMillis, expirationTimeObject)) {
919 remove(key);
920 }
921 }
922
923 /**
924 * All expired entries are removed from the map prior to returning the size.
925 * {@inheritDoc}
926 */
927 @Override
928 public int size() {
929 removeAllExpired(now());
930 return super.size();
931 }
932
933 /**
934 * All expired entries are removed from the map prior to returning the value collection.
935 * {@inheritDoc}
936 */
937 @Override
938 public Collection<V> values() {
939 removeAllExpired(now());
940 return new ValuesCollection(super.values());
941 }
942
943 /**
944 * Serializes this object to an ObjectOutputStream.
945 *
946 * @param out The target ObjectOutputStream.
947 * @throws IOException thrown when an I/O errors occur writing to the target stream.
948 */
949 private void writeObject(final ObjectOutputStream out)
950 throws IOException {
951 out.defaultWriteObject();
952 out.writeObject(map);
953 }
954 }