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 *      https://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.Objects;
028import java.util.Set;
029import java.util.concurrent.TimeUnit;
030import java.util.function.Predicate;
031
032import org.apache.commons.collections4.collection.AbstractCollectionDecorator;
033import org.apache.commons.collections4.iterators.AbstractIteratorDecorator;
034import org.apache.commons.collections4.set.AbstractSetDecorator;
035
036/**
037 * Decorates a {@code Map} to evict expired entries once their expiration
038 * time has been reached.
039 * <p>
040 * When putting a key-value pair in the map this decorator uses a
041 * {@link ExpirationPolicy} to determine how long the entry should remain alive
042 * as defined by an expiration time value.
043 * </p>
044 * <p>
045 * When accessing the mapped value for a key, its expiration time is checked,
046 * and if it is a negative value or if it is greater than the current time, the
047 * mapped value is returned. Otherwise, the key is removed from the decorated
048 * map, and {@code null} is returned.
049 * </p>
050 * <p>
051 * When invoking methods that involve accessing the entire map contents (i.e
052 * {@link #containsValue(Object)}, {@link #entrySet()}, etc.) this decorator
053 * removes all expired entries prior to actually completing the invocation.
054 * </p>
055 * <p>
056 * <strong>Note that {@link PassiveExpiringMap} is not synchronized and is not
057 * thread-safe.</strong> If you wish to use this map from multiple threads
058 * concurrently, you must use appropriate synchronization. The simplest approach
059 * is to wrap this map using {@link java.util.Collections#synchronizedMap(Map)}.
060 * This class may throw exceptions when accessed by concurrent threads without
061 * synchronization.
062 * </p>
063 *
064 * @param <K> The type of the keys in this map
065 * @param <V> The type of the values in this map
066 * @since 4.0
067 */
068public class PassiveExpiringMap<K, V>
069    extends AbstractMapDecorator<K, V>
070    implements Serializable {
071
072    /**
073     * A {@link ExpirationPolicy ExpirationPolicy}
074     * that returns an expiration time that is a
075     * constant about of time in the future from the current time.
076     *
077     * @param <K> The type of the keys in the map
078     * @param <V> The type of the values in the map
079     * @since 4.0
080     */
081    public static class ConstantTimeToLiveExpirationPolicy<K, V>
082        implements ExpirationPolicy<K, V> {
083
084        /** Serialization version */
085        private static final long serialVersionUID = 1L;
086
087        /** The constant time-to-live value measured in milliseconds. */
088        private final long timeToLiveMillis;
089
090        /**
091         * Default constructor. Constructs a policy using a negative
092         * time-to-live value that results in entries never expiring.
093         */
094        public ConstantTimeToLiveExpirationPolicy() {
095            this(-1L);
096        }
097
098        /**
099         * 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} &ge; 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 &ge; 0
811     *         and {@code expirationTimeObject} &lt; {@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}