PassiveExpiringMap.java

  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. import java.io.IOException;
  19. import java.io.ObjectInputStream;
  20. import java.io.ObjectOutputStream;
  21. import java.io.Serializable;
  22. import java.util.Collection;
  23. import java.util.HashMap;
  24. import java.util.Iterator;
  25. import java.util.Map;
  26. import java.util.Objects;
  27. import java.util.Set;
  28. import java.util.concurrent.TimeUnit;

  29. /**
  30.  * Decorates a {@code Map} to evict expired entries once their expiration
  31.  * time has been reached.
  32.  * <p>
  33.  * When putting a key-value pair in the map this decorator uses a
  34.  * {@link ExpirationPolicy} to determine how long the entry should remain alive
  35.  * as defined by an expiration time value.
  36.  * </p>
  37.  * <p>
  38.  * When accessing the mapped value for a key, its expiration time is checked,
  39.  * and if it is a negative value or if it is greater than the current time, the
  40.  * mapped value is returned. Otherwise, the key is removed from the decorated
  41.  * map, and {@code null} is returned.
  42.  * </p>
  43.  * <p>
  44.  * When invoking methods that involve accessing the entire map contents (i.e
  45.  * {@link #containsValue(Object)}, {@link #entrySet()}, etc.) this decorator
  46.  * removes all expired entries prior to actually completing the invocation.
  47.  * </p>
  48.  * <p>
  49.  * <strong>Note that {@link PassiveExpiringMap} is not synchronized and is not
  50.  * thread-safe.</strong> If you wish to use this map from multiple threads
  51.  * concurrently, you must use appropriate synchronization. The simplest approach
  52.  * is to wrap this map using {@link java.util.Collections#synchronizedMap(Map)}.
  53.  * This class may throw exceptions when accessed by concurrent threads without
  54.  * synchronization.
  55.  * </p>
  56.  *
  57.  * @param <K> the type of the keys in this map
  58.  * @param <V> the type of the values in this map
  59.  * @since 4.0
  60.  */
  61. public class PassiveExpiringMap<K, V>
  62.     extends AbstractMapDecorator<K, V>
  63.     implements Serializable {

  64.     /**
  65.      * A {@link org.apache.commons.collections4.map.PassiveExpiringMap.ExpirationPolicy ExpirationPolicy}
  66.      * that returns an expiration time that is a
  67.      * constant about of time in the future from the current time.
  68.      *
  69.      * @param <K> the type of the keys in the map
  70.      * @param <V> the type of the values in the map
  71.      * @since 4.0
  72.      */
  73.     public static class ConstantTimeToLiveExpirationPolicy<K, V>
  74.         implements ExpirationPolicy<K, V> {

  75.         /** Serialization version */
  76.         private static final long serialVersionUID = 1L;

  77.         /** The constant time-to-live value measured in milliseconds. */
  78.         private final long timeToLiveMillis;

  79.         /**
  80.          * Default constructor. Constructs a policy using a negative
  81.          * time-to-live value that results in entries never expiring.
  82.          */
  83.         public ConstantTimeToLiveExpirationPolicy() {
  84.             this(-1L);
  85.         }

  86.         /**
  87.          * Constructs a policy with the given time-to-live constant measured in
  88.          * milliseconds. A negative time-to-live value indicates entries never
  89.          * expire. A zero time-to-live value indicates entries expire (nearly)
  90.          * immediately.
  91.          *
  92.          * @param timeToLiveMillis the constant amount of time (in milliseconds)
  93.          *        an entry is available before it expires. A negative value
  94.          *        results in entries that NEVER expire. A zero value results in
  95.          *        entries that ALWAYS expire.
  96.          */
  97.         public ConstantTimeToLiveExpirationPolicy(final long timeToLiveMillis) {
  98.             this.timeToLiveMillis = timeToLiveMillis;
  99.         }

  100.         /**
  101.          * Constructs a policy with the given time-to-live constant measured in
  102.          * the given time unit of measure.
  103.          *
  104.          * @param timeToLive the constant amount of time an entry is available
  105.          *        before it expires. A negative value results in entries that
  106.          *        NEVER expire. A zero value results in entries that ALWAYS
  107.          *        expire.
  108.          * @param timeUnit the unit of time for the {@code timeToLive}
  109.          *        parameter, must not be null.
  110.          * @throws NullPointerException if the time unit is null.
  111.          */
  112.         public ConstantTimeToLiveExpirationPolicy(final long timeToLive,
  113.                                                   final TimeUnit timeUnit) {
  114.             this(validateAndConvertToMillis(timeToLive, timeUnit));
  115.         }

  116.         /**
  117.          * Determine the expiration time for the given key-value entry.
  118.          *
  119.          * @param key the key for the entry (ignored).
  120.          * @param value the value for the entry (ignored).
  121.          * @return if {@link #timeToLiveMillis} &ge; 0, an expiration time of
  122.          *         {@link #timeToLiveMillis} +
  123.          *         {@link System#currentTimeMillis()} is returned. Otherwise, -1
  124.          *         is returned indicating the entry never expires.
  125.          */
  126.         @Override
  127.         public long expirationTime(final K key, final V value) {
  128.             if (timeToLiveMillis >= 0L) {
  129.                 // avoid numerical overflow
  130.                 final long nowMillis = System.currentTimeMillis();
  131.                 if (nowMillis > Long.MAX_VALUE - timeToLiveMillis) {
  132.                     // expiration would be greater than Long.MAX_VALUE
  133.                     // never expire
  134.                     return -1;
  135.                 }

  136.                 // timeToLiveMillis in the future
  137.                 return nowMillis + timeToLiveMillis;
  138.             }

  139.             // never expire
  140.             return -1L;
  141.         }
  142.     }

  143.     /**
  144.      * A policy to determine the expiration time for key-value entries.
  145.      *
  146.      * @param <K> the key object type.
  147.      * @param <V> the value object type
  148.      * @since 4.0
  149.      */
  150.     @FunctionalInterface
  151.     public interface ExpirationPolicy<K, V>
  152.         extends Serializable {

  153.         /**
  154.          * Determine the expiration time for the given key-value entry.
  155.          *
  156.          * @param key the key for the entry.
  157.          * @param value the value for the entry.
  158.          * @return the expiration time value measured in milliseconds. A
  159.          *         negative return value indicates the entry never expires.
  160.          */
  161.         long expirationTime(K key, V value);
  162.     }

  163.     /** Serialization version */
  164.     private static final long serialVersionUID = 1L;

  165.     /**
  166.      * First validate the input parameters. If the parameters are valid, convert
  167.      * the given time measured in the given units to the same time measured in
  168.      * milliseconds.
  169.      *
  170.      * @param timeToLive the constant amount of time an entry is available
  171.      *        before it expires. A negative value results in entries that NEVER
  172.      *        expire. A zero value results in entries that ALWAYS expire.
  173.      * @param timeUnit the unit of time for the {@code timeToLive}
  174.      *        parameter, must not be null.
  175.      * @throws NullPointerException if the time unit is null.
  176.      */
  177.     private static long validateAndConvertToMillis(final long timeToLive,
  178.                                                    final TimeUnit timeUnit) {
  179.         Objects.requireNonNull(timeUnit, "timeUnit");
  180.         return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit);
  181.     }

  182.     /** Map used to manage expiration times for the actual map entries. */
  183.     private final Map<Object, Long> expirationMap = new HashMap<>();

  184.     /** The policy used to determine time-to-live values for map entries. */
  185.     private final ExpirationPolicy<K, V> expiringPolicy;

  186.     /**
  187.      * Default constructor. Constructs a map decorator that results in entries
  188.      * NEVER expiring.
  189.      */
  190.     public PassiveExpiringMap() {
  191.         this(-1L);
  192.     }

  193.     /**
  194.      * Constructs a map decorator using the given expiration policy to determine
  195.      * expiration times.
  196.      *
  197.      * @param expiringPolicy the policy used to determine expiration times of
  198.      *        entries as they are added.
  199.      * @throws NullPointerException if expiringPolicy is null
  200.      */
  201.     public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) {
  202.         this(expiringPolicy, new HashMap<>());
  203.     }

  204.     /**
  205.      * Constructs a map decorator that decorates the given map and uses the given
  206.      * expiration policy to determine expiration times. If there are any
  207.      * elements already in the map being decorated, they will NEVER expire
  208.      * unless they are replaced.
  209.      *
  210.      * @param expiringPolicy the policy used to determine expiration times of
  211.      *        entries as they are added.
  212.      * @param map the map to decorate, must not be null.
  213.      * @throws NullPointerException if the map or expiringPolicy is null.
  214.      */
  215.     public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy,
  216.                               final Map<K, V> map) {
  217.         super(map);
  218.         this.expiringPolicy = Objects.requireNonNull(expiringPolicy, "expiringPolicy");
  219.     }

  220.     /**
  221.      * Constructs a map decorator that decorates the given map using the given
  222.      * time-to-live value measured in milliseconds to create and use a
  223.      * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
  224.      *
  225.      * @param timeToLiveMillis the constant amount of time (in milliseconds) an
  226.      *        entry is available before it expires. A negative value results in
  227.      *        entries that NEVER expire. A zero value results in entries that
  228.      *        ALWAYS expire.
  229.      */
  230.     public PassiveExpiringMap(final long timeToLiveMillis) {
  231.         this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
  232.                 new HashMap<>());
  233.     }

  234.     /**
  235.      * Constructs a map decorator using the given time-to-live value measured in
  236.      * milliseconds to create and use a
  237.      * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there
  238.      * are any elements already in the map being decorated, they will NEVER
  239.      * expire unless they are replaced.
  240.      *
  241.      * @param timeToLiveMillis the constant amount of time (in milliseconds) an
  242.      *        entry is available before it expires. A negative value results in
  243.      *        entries that NEVER expire. A zero value results in entries that
  244.      *        ALWAYS expire.
  245.      * @param map the map to decorate, must not be null.
  246.      * @throws NullPointerException if the map is null.
  247.      */
  248.     public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V> map) {
  249.         this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
  250.              map);
  251.     }

  252.     /**
  253.      * Constructs a map decorator using the given time-to-live value measured in
  254.      * the given time units of measure to create and use a
  255.      * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
  256.      *
  257.      * @param timeToLive the constant amount of time an entry is available
  258.      *        before it expires. A negative value results in entries that NEVER
  259.      *        expire. A zero value results in entries that ALWAYS expire.
  260.      * @param timeUnit the unit of time for the {@code timeToLive}
  261.      *        parameter, must not be null.
  262.      * @throws NullPointerException if the time unit is null.
  263.      */
  264.     public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) {
  265.         this(validateAndConvertToMillis(timeToLive, timeUnit));
  266.     }

  267.     /**
  268.      * Constructs a map decorator that decorates the given map using the given
  269.      * time-to-live value measured in the given time units of measure to create
  270.      * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This policy
  271.      * is used to determine expiration times. If there are any elements already
  272.      * in the map being decorated, they will NEVER expire unless they are
  273.      * replaced.
  274.      *
  275.      * @param timeToLive the constant amount of time an entry is available
  276.      *        before it expires. A negative value results in entries that NEVER
  277.      *        expire. A zero value results in entries that ALWAYS expire.
  278.      * @param timeUnit the unit of time for the {@code timeToLive}
  279.      *        parameter, must not be null.
  280.      * @param map the map to decorate, must not be null.
  281.      * @throws NullPointerException if the map or time unit is null.
  282.      */
  283.     public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit, final Map<K, V> map) {
  284.         this(validateAndConvertToMillis(timeToLive, timeUnit), map);
  285.     }

  286.     /**
  287.      * Constructs a map decorator that decorates the given map and results in
  288.      * entries NEVER expiring. If there are any elements already in the map
  289.      * being decorated, they also will NEVER expire.
  290.      *
  291.      * @param map the map to decorate, must not be null.
  292.      * @throws NullPointerException if the map is null.
  293.      */
  294.     public PassiveExpiringMap(final Map<K, V> map) {
  295.         this(-1L, map);
  296.     }

  297.     /**
  298.      * Normal {@link Map#clear()} behavior with the addition of clearing all
  299.      * expiration entries as well.
  300.      */
  301.     @Override
  302.     public void clear() {
  303.         super.clear();
  304.         expirationMap.clear();
  305.     }

  306.     /**
  307.      * All expired entries are removed from the map prior to determining the
  308.      * contains result.
  309.      * {@inheritDoc}
  310.      */
  311.     @Override
  312.     public boolean containsKey(final Object key) {
  313.         removeIfExpired(key, now());
  314.         return super.containsKey(key);
  315.     }

  316.     /**
  317.      * All expired entries are removed from the map prior to determining the
  318.      * contains result.
  319.      * {@inheritDoc}
  320.      */
  321.     @Override
  322.     public boolean containsValue(final Object value) {
  323.         removeAllExpired(now());
  324.         return super.containsValue(value);
  325.     }

  326.     /**
  327.      * All expired entries are removed from the map prior to returning the entry set.
  328.      * {@inheritDoc}
  329.      */
  330.     @Override
  331.     public Set<Entry<K, V>> entrySet() {
  332.         removeAllExpired(now());
  333.         return super.entrySet();
  334.     }

  335.     /**
  336.      * All expired entries are removed from the map prior to returning the entry value.
  337.      * {@inheritDoc}
  338.      */
  339.     @Override
  340.     public V get(final Object key) {
  341.         removeIfExpired(key, now());
  342.         return super.get(key);
  343.     }

  344.     /**
  345.      * All expired entries are removed from the map prior to determining if it is empty.
  346.      * {@inheritDoc}
  347.      */
  348.     @Override
  349.     public boolean isEmpty() {
  350.         removeAllExpired(now());
  351.         return super.isEmpty();
  352.     }

  353.     /**
  354.      * Determines if the given expiration time is less than {@code now}.
  355.      *
  356.      * @param now the time in milliseconds used to compare against the
  357.      *        expiration time.
  358.      * @param expirationTimeObject the expiration time value retrieved from
  359.      *        {@link #expirationMap}, can be null.
  360.      * @return {@code true} if {@code expirationTimeObject} is &ge; 0
  361.      *         and {@code expirationTimeObject} &lt; {@code now}.
  362.      *         {@code false} otherwise.
  363.      */
  364.     private boolean isExpired(final long now, final Long expirationTimeObject) {
  365.         if (expirationTimeObject != null) {
  366.             final long expirationTime = expirationTimeObject.longValue();
  367.             return expirationTime >= 0 && now >= expirationTime;
  368.         }
  369.         return false;
  370.     }

  371.     /**
  372.      * All expired entries are removed from the map prior to returning the key set.
  373.      * {@inheritDoc}
  374.      */
  375.     @Override
  376.     public Set<K> keySet() {
  377.         removeAllExpired(now());
  378.         return super.keySet();
  379.     }

  380.     /**
  381.      * The current time in milliseconds.
  382.      */
  383.     private long now() {
  384.         return System.currentTimeMillis();
  385.     }

  386.     /**
  387.      * {@inheritDoc}
  388.      * <p>
  389.      * 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
  390.      * {@link #expiringPolicy}.
  391.      * </p>
  392.      */
  393.     @Override
  394.     public V put(final K key, final V value) {
  395.         // remove the previous record
  396.         removeIfExpired(key, now());

  397.         // record expiration time of new entry
  398.         final long expirationTime = expiringPolicy.expirationTime(key, value);
  399.         expirationMap.put(key, Long.valueOf(expirationTime));

  400.         return super.put(key, value);
  401.     }

  402.     @Override
  403.     public void putAll(final Map<? extends K, ? extends V> mapToCopy) {
  404.         for (final Map.Entry<? extends K, ? extends V> entry : mapToCopy.entrySet()) {
  405.             put(entry.getKey(), entry.getValue());
  406.         }
  407.     }

  408.     /**
  409.      * Deserializes the map in using a custom routine.
  410.      *
  411.      * @param in the input stream
  412.      * @throws IOException if an error occurs while reading from the stream
  413.      * @throws ClassNotFoundException if an object read from the stream cannot be loaded
  414.      */
  415.     @SuppressWarnings("unchecked")
  416.     // (1) should only fail if input stream is incorrect
  417.     private void readObject(final ObjectInputStream in)
  418.         throws IOException, ClassNotFoundException {
  419.         in.defaultReadObject();
  420.         map = (Map<K, V>) in.readObject(); // (1)
  421.     }

  422.     /**
  423.      * Normal {@link Map#remove(Object)} behavior with the addition of removing
  424.      * any expiration entry as well.
  425.      * {@inheritDoc}
  426.      */
  427.     @Override
  428.     public V remove(final Object key) {
  429.         expirationMap.remove(key);
  430.         return super.remove(key);
  431.     }

  432.     /**
  433.      * Removes all entries in the map whose expiration time is less than
  434.      * {@code now}. The exceptions are entries with negative expiration
  435.      * times; those entries are never removed.
  436.      *
  437.      * @see #isExpired(long, Long)
  438.      */
  439.     private void removeAllExpired(final long nowMillis) {
  440.         final Iterator<Map.Entry<Object, Long>> iter = expirationMap.entrySet().iterator();
  441.         while (iter.hasNext()) {
  442.             final Map.Entry<Object, Long> expirationEntry = iter.next();
  443.             if (isExpired(nowMillis, expirationEntry.getValue())) {
  444.                 // remove entry from collection
  445.                 super.remove(expirationEntry.getKey());
  446.                 // remove entry from expiration map
  447.                 iter.remove();
  448.             }
  449.         }
  450.     }

  451.     /**
  452.      * Removes the entry with the given key if the entry's expiration time is
  453.      * less than {@code now}. If the entry has a negative expiration time,
  454.      * the entry is never removed.
  455.      */
  456.     private void removeIfExpired(final Object key, final long nowMillis) {
  457.         final Long expirationTimeObject = expirationMap.get(key);
  458.         if (isExpired(nowMillis, expirationTimeObject)) {
  459.             remove(key);
  460.         }
  461.     }

  462.     /**
  463.      * All expired entries are removed from the map prior to returning the size.
  464.      * {@inheritDoc}
  465.      */
  466.     @Override
  467.     public int size() {
  468.         removeAllExpired(now());
  469.         return super.size();
  470.     }

  471.     /**
  472.      * All expired entries are removed from the map prior to returning the value collection.
  473.      * {@inheritDoc}
  474.      */
  475.     @Override
  476.     public Collection<V> values() {
  477.         removeAllExpired(now());
  478.         return super.values();
  479.     }

  480.     /**
  481.      * Serializes this object to an ObjectOutputStream.
  482.      *
  483.      * @param out the target ObjectOutputStream.
  484.      * @throws IOException thrown when an I/O errors occur writing to the target stream.
  485.      */
  486.     private void writeObject(final ObjectOutputStream out)
  487.         throws IOException {
  488.         out.defaultWriteObject();
  489.         out.writeObject(map);
  490.     }
  491. }