This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-collections.git
The following commit(s) were added to refs/heads/master by this push:
new b219ccbe7 [COLLECTIONS-776] Fix expiration bypass in
PassiveExpiringMap when wrapped in SynchronizedMap (#694).
b219ccbe7 is described below
commit b219ccbe7b95250abd3ba3143edf340b7fad1943
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Jul 5 15:54:07 2026 -0400
[COLLECTIONS-776] Fix expiration bypass in PassiveExpiringMap when
wrapped in SynchronizedMap (#694).
Sort members.
---
src/changes/changes.xml | 1 +
.../collections4/map/PassiveExpiringMap.java | 1158 ++++++++++----------
.../collections4/map/PassiveExpiringMapTest.java | 342 +++---
3 files changed, 751 insertions(+), 750 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 24535388a..53f8208db 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -69,6 +69,7 @@
<action type="fix" dev="ggregory" due-to="Dexter.k, Gary Gregory">Fix
stale size in AbstractMapMultiSet view iterator remove (#701).</action>
<action type="fix" dev="ggregory" due-to="Javid Khan, Gary Gregory">Stop
ExtendedMessageFormat seekNonWs reading past the pattern end (#1740).</action>
<action type="fix" dev="ggregory" due-to="Dexter.k, Gary Gregory">Remove
stale keys in OrderedProperties compute and merge (#702).</action>
+ <action type="fix" dev="ggregory" due-to="sankalp suman, Gary Gregory"
issue="COLLECTIONS-776">Fix expiration bypass in PassiveExpiringMap when
wrapped in SynchronizedMap (#694).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary Gregory">Add generics to
UnmodifiableIterator for the wrapped type.</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add a Maven
benchmark profile for JMH.</action>
diff --git
a/src/main/java/org/apache/commons/collections4/map/PassiveExpiringMap.java
b/src/main/java/org/apache/commons/collections4/map/PassiveExpiringMap.java
index eb1eb1c7f..feacaf26c 100644
--- a/src/main/java/org/apache/commons/collections4/map/PassiveExpiringMap.java
+++ b/src/main/java/org/apache/commons/collections4/map/PassiveExpiringMap.java
@@ -157,6 +157,150 @@ public class PassiveExpiringMap<K, V>
}
}
+ private final class EntrySet extends AbstractSetDecorator<Entry<K, V>> {
+
+ /** Generated serial version ID. */
+ private static final long serialVersionUID = 1L;
+
+ private EntrySet(final Set<Entry<K, V>> set) {
+ super(set);
+ }
+
+ @Override
+ public void clear() {
+ PassiveExpiringMap.this.clear();
+ }
+
+ @Override
+ public boolean contains(final Object object) {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.contains(object);
+ }
+
+ @Override
+ public boolean containsAll(final Collection<?> coll) {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.containsAll(coll);
+ }
+
+ @Override
+ public boolean isEmpty() {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.isEmpty();
+ }
+
+ @Override
+ public Iterator<Entry<K, V>> iterator() {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return new EntrySetIterator(super.iterator());
+ }
+
+ @Override
+ public boolean remove(final Object object) {
+ if (object instanceof Map.Entry) {
+ final Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
+ final Object key = entry.getKey();
+ if (PassiveExpiringMap.this.containsKey(key)) {
+ final Object value = PassiveExpiringMap.this.get(key);
+ if (Objects.equals(value, entry.getValue())) {
+ PassiveExpiringMap.this.remove(key);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(final Collection<?> coll) {
+ Objects.requireNonNull(coll);
+ boolean changed = false;
+ if (size() > coll.size()) {
+ for (final Object obj : coll) {
+ changed |= remove(obj);
+ }
+ } else {
+ final Iterator<?> it = iterator();
+ while (it.hasNext()) {
+ if (coll.contains(it.next())) {
+ it.remove();
+ changed = true;
+ }
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean removeIf(final Predicate<? super Entry<K, V>> filter) {
+ Objects.requireNonNull(filter);
+ boolean changed = false;
+ final Iterator<Entry<K, V>> it = iterator();
+ while (it.hasNext()) {
+ if (filter.test(it.next())) {
+ it.remove();
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean retainAll(final Collection<?> coll) {
+ Objects.requireNonNull(coll);
+ boolean changed = false;
+ final Iterator<?> it = iterator();
+ while (it.hasNext()) {
+ if (!coll.contains(it.next())) {
+ it.remove();
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public int size() {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.size();
+ }
+
+ @Override
+ public Object[] toArray() {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(final T[] array) {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.toArray(array);
+ }
+ }
+
+ private final class EntrySetIterator extends
AbstractIteratorDecorator<Entry<K, V>> {
+ private Entry<K, V> lastReturned;
+
+ private EntrySetIterator(final Iterator<Entry<K, V>> iterator) {
+ super(iterator);
+ }
+
+ @Override
+ public Entry<K, V> next() {
+ lastReturned = super.next();
+ return lastReturned;
+ }
+
+ @Override
+ public void remove() {
+ super.remove();
+ if (lastReturned != null) {
+
PassiveExpiringMap.this.expirationMap.remove(lastReturned.getKey());
+ lastReturned = null;
+ }
+ }
+ }
+
/**
* A policy to determine the expiration time for key-value entries.
*
@@ -179,413 +323,50 @@ public class PassiveExpiringMap<K, V>
long expirationTime(K key, V value);
}
- /** Serialization version */
- private static final long serialVersionUID = 1L;
+ private final class KeySet extends AbstractSetDecorator<K> {
- /**
- * First validate the input parameters. If the parameters are valid,
convert
- * the given time measured in the given units to the same time measured in
- * milliseconds.
- *
- * @param timeToLive the constant amount of time an entry is available
- * before it expires. A negative value results in entries that NEVER
- * expire. A zero value results in entries that ALWAYS expire.
- * @param timeUnit the unit of time for the {@code timeToLive}
- * parameter, must not be null.
- * @throws NullPointerException if the time unit is null.
- */
- private static long validateAndConvertToMillis(final long timeToLive,
- final TimeUnit timeUnit) {
- Objects.requireNonNull(timeUnit, "timeUnit");
- return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit);
- }
+ /** Generated serial version ID. */
+ private static final long serialVersionUID = 1L;
- /** Map used to manage expiration times for the actual map entries. */
- private final Map<Object, Long> expirationMap = new HashMap<>();
+ private KeySet(final Set<K> set) {
+ super(set);
+ }
- /** The policy used to determine time-to-live values for map entries. */
- private final ExpirationPolicy<K, V> expiringPolicy;
+ @Override
+ public void clear() {
+ PassiveExpiringMap.this.clear();
+ }
- /**
- * Default constructor. Constructs a map decorator that results in entries
- * NEVER expiring.
- */
- public PassiveExpiringMap() {
- this(-1L);
- }
+ @Override
+ public boolean contains(final Object key) {
+ PassiveExpiringMap.this.removeIfExpired(key,
PassiveExpiringMap.this.now());
+ return super.contains(key);
+ }
- /**
- * Constructs a map decorator using the given expiration policy to
determine
- * expiration times.
- *
- * @param expiringPolicy the policy used to determine expiration times of
- * entries as they are added.
- * @throws NullPointerException if expiringPolicy is null
- */
- public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) {
- this(expiringPolicy, new HashMap<>());
- }
+ @Override
+ public boolean containsAll(final Collection<?> coll) {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.containsAll(coll);
+ }
- /**
- * Constructs a map decorator that decorates the given map and uses the
given
- * expiration policy to determine expiration times. If there are any
- * elements already in the map being decorated, they will NEVER expire
- * unless they are replaced.
- *
- * @param expiringPolicy the policy used to determine expiration times of
- * entries as they are added.
- * @param map the map to decorate, must not be null.
- * @throws NullPointerException if the map or expiringPolicy is null.
- */
- public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy,
- final Map<K, V> map) {
- super(map);
- this.expiringPolicy = Objects.requireNonNull(expiringPolicy,
"expiringPolicy");
- }
+ @Override
+ public boolean isEmpty() {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.isEmpty();
+ }
- /**
- * Constructs a map decorator that decorates the given map using the given
- * time-to-live value measured in milliseconds to create and use a
- * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
- *
- * @param timeToLiveMillis the constant amount of time (in milliseconds) an
- * entry is available before it expires. A negative value results in
- * entries that NEVER expire. A zero value results in entries that
- * ALWAYS expire.
- */
- public PassiveExpiringMap(final long timeToLiveMillis) {
- this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
- new HashMap<>());
- }
-
- /**
- * Constructs a map decorator using the given time-to-live value measured
in
- * milliseconds to create and use a
- * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there
- * are any elements already in the map being decorated, they will NEVER
- * expire unless they are replaced.
- *
- * @param timeToLiveMillis the constant amount of time (in milliseconds) an
- * entry is available before it expires. A negative value results in
- * entries that NEVER expire. A zero value results in entries that
- * ALWAYS expire.
- * @param map the map to decorate, must not be null.
- * @throws NullPointerException if the map is null.
- */
- public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V>
map) {
- this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
- map);
- }
-
- /**
- * Constructs a map decorator using the given time-to-live value measured
in
- * the given time units of measure to create and use a
- * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
- *
- * @param timeToLive the constant amount of time an entry is available
- * before it expires. A negative value results in entries that NEVER
- * expire. A zero value results in entries that ALWAYS expire.
- * @param timeUnit the unit of time for the {@code timeToLive}
- * parameter, must not be null.
- * @throws NullPointerException if the time unit is null.
- */
- public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) {
- this(validateAndConvertToMillis(timeToLive, timeUnit));
- }
-
- /**
- * Constructs a map decorator that decorates the given map using the given
- * time-to-live value measured in the given time units of measure to create
- * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This
policy
- * is used to determine expiration times. If there are any elements already
- * in the map being decorated, they will NEVER expire unless they are
- * replaced.
- *
- * @param timeToLive the constant amount of time an entry is available
- * before it expires. A negative value results in entries that NEVER
- * expire. A zero value results in entries that ALWAYS expire.
- * @param timeUnit the unit of time for the {@code timeToLive}
- * parameter, must not be null.
- * @param map the map to decorate, must not be null.
- * @throws NullPointerException if the map or time unit is null.
- */
- public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit,
final Map<K, V> map) {
- this(validateAndConvertToMillis(timeToLive, timeUnit), map);
- }
-
- /**
- * Constructs a map decorator that decorates the given map and results in
- * entries NEVER expiring. If there are any elements already in the map
- * being decorated, they also will NEVER expire.
- *
- * @param map the map to decorate, must not be null.
- * @throws NullPointerException if the map is null.
- */
- public PassiveExpiringMap(final Map<K, V> map) {
- this(-1L, map);
- }
-
- /**
- * Normal {@link Map#clear()} behavior with the addition of clearing all
- * expiration entries as well.
- */
- @Override
- public void clear() {
- super.clear();
- expirationMap.clear();
- }
-
- /**
- * All expired entries are removed from the map prior to determining the
- * contains result.
- * {@inheritDoc}
- */
- @Override
- public boolean containsKey(final Object key) {
- removeIfExpired(key, now());
- return super.containsKey(key);
- }
-
- /**
- * All expired entries are removed from the map prior to determining the
- * contains result.
- * {@inheritDoc}
- */
- @Override
- public boolean containsValue(final Object value) {
- removeAllExpired(now());
- return super.containsValue(value);
- }
-
- /**
- * All expired entries are removed from the map prior to returning the
entry set.
- * {@inheritDoc}
- */
- @Override
- public Set<Entry<K, V>> entrySet() {
- removeAllExpired(now());
- return new EntrySet(super.entrySet());
- }
-
- /**
- * All expired entries are removed from the map prior to returning the
entry value.
- * {@inheritDoc}
- */
- @Override
- public V get(final Object key) {
- removeIfExpired(key, now());
- return super.get(key);
- }
-
- /**
- * All expired entries are removed from the map prior to determining if it
is empty.
- * {@inheritDoc}
- */
- @Override
- public boolean isEmpty() {
- removeAllExpired(now());
- return super.isEmpty();
- }
-
- /**
- * Determines if the given expiration time is less than {@code now}.
- *
- * @param now the time in milliseconds used to compare against the
- * expiration time.
- * @param expirationTimeObject the expiration time value retrieved from
- * {@link #expirationMap}, can be null.
- * @return {@code true} if {@code expirationTimeObject} is ≥ 0
- * and {@code expirationTimeObject} < {@code now}.
- * {@code false} otherwise.
- */
- private boolean isExpired(final long now, final Long expirationTimeObject)
{
- if (expirationTimeObject != null) {
- final long expirationTime = expirationTimeObject.longValue();
- return expirationTime >= 0 && now >= expirationTime;
- }
- return false;
- }
-
- /**
- * All expired entries are removed from the map prior to returning the key
set.
- * {@inheritDoc}
- */
- @Override
- public Set<K> keySet() {
- removeAllExpired(now());
- return new KeySet(super.keySet());
- }
-
- /**
- * The current time in milliseconds.
- */
- private long now() {
- return System.currentTimeMillis();
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * 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
- * {@link #expiringPolicy}.
- * </p>
- */
- @Override
- public V put(final K key, final V value) {
- // remove the previous record
- removeIfExpired(key, now());
-
- // record expiration time of new entry
- final long expirationTime = expiringPolicy.expirationTime(key, value);
- expirationMap.put(key, Long.valueOf(expirationTime));
-
- return super.put(key, value);
- }
-
- @Override
- public void putAll(final Map<? extends K, ? extends V> mapToCopy) {
- for (final Map.Entry<? extends K, ? extends V> entry :
mapToCopy.entrySet()) {
- put(entry.getKey(), entry.getValue());
- }
- }
-
- /**
- * Deserializes the map in using a custom routine.
- *
- * @param in the input stream
- * @throws IOException if an error occurs while reading from the stream
- * @throws ClassNotFoundException if an object read from the stream cannot
be loaded
- */
- @SuppressWarnings("unchecked")
- // (1) should only fail if input stream is incorrect
- private void readObject(final ObjectInputStream in)
- throws IOException, ClassNotFoundException {
- in.defaultReadObject();
- map = (Map<K, V>) in.readObject(); // (1)
- }
-
- /**
- * Normal {@link Map#remove(Object)} behavior with the addition of removing
- * any expiration entry as well.
- * {@inheritDoc}
- */
- @Override
- public V remove(final Object key) {
- expirationMap.remove(key);
- return super.remove(key);
- }
-
- /**
- * Removes all entries in the map whose expiration time is less than
- * {@code now}. The exceptions are entries with negative expiration
- * times; those entries are never removed.
- *
- * @see #isExpired(long, Long)
- */
- private void removeAllExpired(final long nowMillis) {
- final Iterator<Map.Entry<Object, Long>> iter =
expirationMap.entrySet().iterator();
- while (iter.hasNext()) {
- final Map.Entry<Object, Long> expirationEntry = iter.next();
- if (isExpired(nowMillis, expirationEntry.getValue())) {
- // remove entry from collection
- super.remove(expirationEntry.getKey());
- // remove entry from expiration map
- iter.remove();
- }
- }
- }
-
- /**
- * Removes the entry with the given key if the entry's expiration time is
- * less than {@code now}. If the entry has a negative expiration time,
- * the entry is never removed.
- */
- private void removeIfExpired(final Object key, final long nowMillis) {
- final Long expirationTimeObject = expirationMap.get(key);
- if (isExpired(nowMillis, expirationTimeObject)) {
- remove(key);
- }
- }
-
- /**
- * All expired entries are removed from the map prior to returning the
size.
- * {@inheritDoc}
- */
- @Override
- public int size() {
- removeAllExpired(now());
- return super.size();
- }
-
- /**
- * All expired entries are removed from the map prior to returning the
value collection.
- * {@inheritDoc}
- */
- @Override
- public Collection<V> values() {
- removeAllExpired(now());
- return new ValuesCollection(super.values());
- }
-
- /**
- * Serializes this object to an ObjectOutputStream.
- *
- * @param out the target ObjectOutputStream.
- * @throws IOException thrown when an I/O errors occur writing to the
target stream.
- */
- private void writeObject(final ObjectOutputStream out)
- throws IOException {
- out.defaultWriteObject();
- out.writeObject(map);
- }
-
- private final class EntrySet extends AbstractSetDecorator<Entry<K, V>> {
-
- /** Generated serial version ID. */
- private static final long serialVersionUID = 1L;
-
- private EntrySet(final Set<Entry<K, V>> set) {
- super(set);
- }
-
- @Override
- public Iterator<Entry<K, V>> iterator() {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return new EntrySetIterator(super.iterator());
- }
-
- @Override
- public int size() {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.size();
- }
+ @Override
+ public Iterator<K> iterator() {
+ return new
KeySetIterator(PassiveExpiringMap.this.entrySet().iterator());
+ }
@Override
- public boolean isEmpty() {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.isEmpty();
- }
-
- @Override
- public boolean contains(final Object object) {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.contains(object);
- }
-
- @Override
- public boolean remove(final Object object) {
- if (object instanceof Map.Entry) {
- final Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
- final Object key = entry.getKey();
- if (PassiveExpiringMap.this.containsKey(key)) {
- final Object value = PassiveExpiringMap.this.get(key);
- if (Objects.equals(value, entry.getValue())) {
- PassiveExpiringMap.this.remove(key);
- return true;
- }
- }
+ public boolean remove(final Object key) {
+ final boolean hasKey = contains(key);
+ if (hasKey) {
+ PassiveExpiringMap.this.remove(key);
}
- return false;
+ return hasKey;
}
@Override
@@ -609,12 +390,12 @@ public class PassiveExpiringMap<K, V>
}
@Override
- public boolean retainAll(final Collection<?> coll) {
- Objects.requireNonNull(coll);
+ public boolean removeIf(final Predicate<? super K> filter) {
+ Objects.requireNonNull(filter);
boolean changed = false;
- final Iterator<?> it = iterator();
+ final Iterator<K> it = iterator();
while (it.hasNext()) {
- if (!coll.contains(it.next())) {
+ if (filter.test(it.next())) {
it.remove();
changed = true;
}
@@ -623,12 +404,12 @@ public class PassiveExpiringMap<K, V>
}
@Override
- public boolean removeIf(final Predicate<? super Entry<K, V>> filter) {
- Objects.requireNonNull(filter);
+ public boolean retainAll(final Collection<?> coll) {
+ Objects.requireNonNull(coll);
boolean changed = false;
- final Iterator<Entry<K, V>> it = iterator();
+ final Iterator<?> it = iterator();
while (it.hasNext()) {
- if (filter.test(it.next())) {
+ if (!coll.contains(it.next())) {
it.remove();
changed = true;
}
@@ -637,8 +418,9 @@ public class PassiveExpiringMap<K, V>
}
@Override
- public void clear() {
- PassiveExpiringMap.this.clear();
+ public int size() {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.size();
}
@Override
@@ -652,55 +434,55 @@ public class PassiveExpiringMap<K, V>
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
return super.toArray(array);
}
-
- @Override
- public boolean containsAll(final Collection<?> coll) {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.containsAll(coll);
- }
}
- private final class EntrySetIterator extends
AbstractIteratorDecorator<Entry<K, V>> {
- private Entry<K, V> lastReturned;
+ private final class KeySetIterator implements Iterator<K> {
+ private final Iterator<Entry<K, V>> iterator;
- private EntrySetIterator(final Iterator<Entry<K, V>> iterator) {
- super(iterator);
+ private KeySetIterator(final Iterator<Entry<K, V>> iterator) {
+ this.iterator = iterator;
}
@Override
- public Entry<K, V> next() {
- lastReturned = super.next();
- return lastReturned;
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public K next() {
+ return iterator.next().getKey();
}
@Override
public void remove() {
- super.remove();
- if (lastReturned != null) {
-
PassiveExpiringMap.this.expirationMap.remove(lastReturned.getKey());
- lastReturned = null;
- }
+ iterator.remove();
}
}
- private final class KeySet extends AbstractSetDecorator<K> {
+ private final class ValuesCollection extends
AbstractCollectionDecorator<V> {
/** Generated serial version ID. */
private static final long serialVersionUID = 1L;
- private KeySet(final Set<K> set) {
- super(set);
+ private ValuesCollection(final Collection<V> coll) {
+ super(coll);
}
@Override
- public Iterator<K> iterator() {
- return new
KeySetIterator(PassiveExpiringMap.this.entrySet().iterator());
+ public void clear() {
+ PassiveExpiringMap.this.clear();
}
@Override
- public int size() {
+ public boolean contains(final Object value) {
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.size();
+ return super.contains(value);
+ }
+
+ @Override
+ public boolean containsAll(final Collection<?> coll) {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.containsAll(coll);
}
@Override
@@ -710,47 +492,43 @@ public class PassiveExpiringMap<K, V>
}
@Override
- public boolean contains(final Object key) {
- PassiveExpiringMap.this.removeIfExpired(key,
PassiveExpiringMap.this.now());
- return super.contains(key);
+ public Iterator<V> iterator() {
+ return new
ValuesIterator(PassiveExpiringMap.this.entrySet().iterator());
}
@Override
- public boolean remove(final Object key) {
- final boolean hasKey = contains(key);
- if (hasKey) {
- PassiveExpiringMap.this.remove(key);
+ public boolean remove(final Object value) {
+ final Iterator<V> it = iterator();
+ while (it.hasNext()) {
+ if (Objects.equals(it.next(), value)) {
+ it.remove();
+ return true;
+ }
}
- return hasKey;
+ return false;
}
@Override
public boolean removeAll(final Collection<?> coll) {
Objects.requireNonNull(coll);
boolean changed = false;
- if (size() > coll.size()) {
- for (final Object obj : coll) {
- changed |= remove(obj);
- }
- } else {
- final Iterator<?> it = iterator();
- while (it.hasNext()) {
- if (coll.contains(it.next())) {
- it.remove();
- changed = true;
- }
+ final Iterator<?> it = iterator();
+ while (it.hasNext()) {
+ if (coll.contains(it.next())) {
+ it.remove();
+ changed = true;
}
}
return changed;
}
@Override
- public boolean retainAll(final Collection<?> coll) {
- Objects.requireNonNull(coll);
+ public boolean removeIf(final Predicate<? super V> filter) {
+ Objects.requireNonNull(filter);
boolean changed = false;
- final Iterator<?> it = iterator();
+ final Iterator<V> it = iterator();
while (it.hasNext()) {
- if (!coll.contains(it.next())) {
+ if (filter.test(it.next())) {
it.remove();
changed = true;
}
@@ -759,12 +537,12 @@ public class PassiveExpiringMap<K, V>
}
@Override
- public boolean removeIf(final Predicate<? super K> filter) {
- Objects.requireNonNull(filter);
+ public boolean retainAll(final Collection<?> coll) {
+ Objects.requireNonNull(coll);
boolean changed = false;
- final Iterator<K> it = iterator();
+ final Iterator<?> it = iterator();
while (it.hasNext()) {
- if (filter.test(it.next())) {
+ if (!coll.contains(it.next())) {
it.remove();
changed = true;
}
@@ -773,8 +551,9 @@ public class PassiveExpiringMap<K, V>
}
@Override
- public void clear() {
- PassiveExpiringMap.this.clear();
+ public int size() {
+
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+ return super.size();
}
@Override
@@ -788,18 +567,12 @@ public class PassiveExpiringMap<K, V>
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
return super.toArray(array);
}
-
- @Override
- public boolean containsAll(final Collection<?> coll) {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.containsAll(coll);
- }
}
- private final class KeySetIterator implements Iterator<K> {
+ private final class ValuesIterator implements Iterator<V> {
private final Iterator<Entry<K, V>> iterator;
- private KeySetIterator(final Iterator<Entry<K, V>> iterator) {
+ private ValuesIterator(final Iterator<Entry<K, V>> iterator) {
this.iterator = iterator;
}
@@ -809,8 +582,8 @@ public class PassiveExpiringMap<K, V>
}
@Override
- public K next() {
- return iterator.next().getKey();
+ public V next() {
+ return iterator.next().getValue();
}
@Override
@@ -819,136 +592,363 @@ public class PassiveExpiringMap<K, V>
}
}
- private final class ValuesCollection extends
AbstractCollectionDecorator<V> {
+ /** Serialization version */
+ private static final long serialVersionUID = 1L;
- /** Generated serial version ID. */
- private static final long serialVersionUID = 1L;
+ /**
+ * First validate the input parameters. If the parameters are valid,
convert
+ * the given time measured in the given units to the same time measured in
+ * milliseconds.
+ *
+ * @param timeToLive the constant amount of time an entry is available
+ * before it expires. A negative value results in entries that NEVER
+ * expire. A zero value results in entries that ALWAYS expire.
+ * @param timeUnit the unit of time for the {@code timeToLive}
+ * parameter, must not be null.
+ * @throws NullPointerException if the time unit is null.
+ */
+ private static long validateAndConvertToMillis(final long timeToLive,
+ final TimeUnit timeUnit) {
+ Objects.requireNonNull(timeUnit, "timeUnit");
+ return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit);
+ }
- private ValuesCollection(final Collection<V> coll) {
- super(coll);
- }
+ /** Map used to manage expiration times for the actual map entries. */
+ private final Map<Object, Long> expirationMap = new HashMap<>();
+
+ /** The policy used to determine time-to-live values for map entries. */
+ private final ExpirationPolicy<K, V> expiringPolicy;
+
+ /**
+ * Default constructor. Constructs a map decorator that results in entries
+ * NEVER expiring.
+ */
+ public PassiveExpiringMap() {
+ this(-1L);
+ }
+
+ /**
+ * Constructs a map decorator using the given expiration policy to
determine
+ * expiration times.
+ *
+ * @param expiringPolicy the policy used to determine expiration times of
+ * entries as they are added.
+ * @throws NullPointerException if expiringPolicy is null
+ */
+ public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) {
+ this(expiringPolicy, new HashMap<>());
+ }
+
+ /**
+ * Constructs a map decorator that decorates the given map and uses the
given
+ * expiration policy to determine expiration times. If there are any
+ * elements already in the map being decorated, they will NEVER expire
+ * unless they are replaced.
+ *
+ * @param expiringPolicy the policy used to determine expiration times of
+ * entries as they are added.
+ * @param map the map to decorate, must not be null.
+ * @throws NullPointerException if the map or expiringPolicy is null.
+ */
+ public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy,
+ final Map<K, V> map) {
+ super(map);
+ this.expiringPolicy = Objects.requireNonNull(expiringPolicy,
"expiringPolicy");
+ }
+
+ /**
+ * Constructs a map decorator that decorates the given map using the given
+ * time-to-live value measured in milliseconds to create and use a
+ * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
+ *
+ * @param timeToLiveMillis the constant amount of time (in milliseconds) an
+ * entry is available before it expires. A negative value results in
+ * entries that NEVER expire. A zero value results in entries that
+ * ALWAYS expire.
+ */
+ public PassiveExpiringMap(final long timeToLiveMillis) {
+ this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
+ new HashMap<>());
+ }
+
+ /**
+ * Constructs a map decorator using the given time-to-live value measured
in
+ * milliseconds to create and use a
+ * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there
+ * are any elements already in the map being decorated, they will NEVER
+ * expire unless they are replaced.
+ *
+ * @param timeToLiveMillis the constant amount of time (in milliseconds) an
+ * entry is available before it expires. A negative value results in
+ * entries that NEVER expire. A zero value results in entries that
+ * ALWAYS expire.
+ * @param map the map to decorate, must not be null.
+ * @throws NullPointerException if the map is null.
+ */
+ public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V>
map) {
+ this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
+ map);
+ }
+
+ /**
+ * Constructs a map decorator using the given time-to-live value measured
in
+ * the given time units of measure to create and use a
+ * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
+ *
+ * @param timeToLive the constant amount of time an entry is available
+ * before it expires. A negative value results in entries that NEVER
+ * expire. A zero value results in entries that ALWAYS expire.
+ * @param timeUnit the unit of time for the {@code timeToLive}
+ * parameter, must not be null.
+ * @throws NullPointerException if the time unit is null.
+ */
+ public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) {
+ this(validateAndConvertToMillis(timeToLive, timeUnit));
+ }
+
+ /**
+ * Constructs a map decorator that decorates the given map using the given
+ * time-to-live value measured in the given time units of measure to create
+ * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This
policy
+ * is used to determine expiration times. If there are any elements already
+ * in the map being decorated, they will NEVER expire unless they are
+ * replaced.
+ *
+ * @param timeToLive the constant amount of time an entry is available
+ * before it expires. A negative value results in entries that NEVER
+ * expire. A zero value results in entries that ALWAYS expire.
+ * @param timeUnit the unit of time for the {@code timeToLive}
+ * parameter, must not be null.
+ * @param map the map to decorate, must not be null.
+ * @throws NullPointerException if the map or time unit is null.
+ */
+ public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit,
final Map<K, V> map) {
+ this(validateAndConvertToMillis(timeToLive, timeUnit), map);
+ }
+
+ /**
+ * Constructs a map decorator that decorates the given map and results in
+ * entries NEVER expiring. If there are any elements already in the map
+ * being decorated, they also will NEVER expire.
+ *
+ * @param map the map to decorate, must not be null.
+ * @throws NullPointerException if the map is null.
+ */
+ public PassiveExpiringMap(final Map<K, V> map) {
+ this(-1L, map);
+ }
+
+ /**
+ * Normal {@link Map#clear()} behavior with the addition of clearing all
+ * expiration entries as well.
+ */
+ @Override
+ public void clear() {
+ super.clear();
+ expirationMap.clear();
+ }
+
+ /**
+ * All expired entries are removed from the map prior to determining the
+ * contains result.
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean containsKey(final Object key) {
+ removeIfExpired(key, now());
+ return super.containsKey(key);
+ }
+
+ /**
+ * All expired entries are removed from the map prior to determining the
+ * contains result.
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean containsValue(final Object value) {
+ removeAllExpired(now());
+ return super.containsValue(value);
+ }
+
+ /**
+ * All expired entries are removed from the map prior to returning the
entry set.
+ * {@inheritDoc}
+ */
+ @Override
+ public Set<Entry<K, V>> entrySet() {
+ removeAllExpired(now());
+ return new EntrySet(super.entrySet());
+ }
- @Override
- public Iterator<V> iterator() {
- return new
ValuesIterator(PassiveExpiringMap.this.entrySet().iterator());
- }
+ /**
+ * All expired entries are removed from the map prior to returning the
entry value.
+ * {@inheritDoc}
+ */
+ @Override
+ public V get(final Object key) {
+ removeIfExpired(key, now());
+ return super.get(key);
+ }
- @Override
- public int size() {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.size();
- }
+ /**
+ * All expired entries are removed from the map prior to determining if it
is empty.
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isEmpty() {
+ removeAllExpired(now());
+ return super.isEmpty();
+ }
- @Override
- public boolean isEmpty() {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.isEmpty();
+ /**
+ * Determines if the given expiration time is less than {@code now}.
+ *
+ * @param now the time in milliseconds used to compare against the
+ * expiration time.
+ * @param expirationTimeObject the expiration time value retrieved from
+ * {@link #expirationMap}, can be null.
+ * @return {@code true} if {@code expirationTimeObject} is ≥ 0
+ * and {@code expirationTimeObject} < {@code now}.
+ * {@code false} otherwise.
+ */
+ private boolean isExpired(final long now, final Long expirationTimeObject)
{
+ if (expirationTimeObject != null) {
+ final long expirationTime = expirationTimeObject.longValue();
+ return expirationTime >= 0 && now >= expirationTime;
}
+ return false;
+ }
- @Override
- public boolean contains(final Object value) {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.contains(value);
- }
+ /**
+ * All expired entries are removed from the map prior to returning the key
set.
+ * {@inheritDoc}
+ */
+ @Override
+ public Set<K> keySet() {
+ removeAllExpired(now());
+ return new KeySet(super.keySet());
+ }
- @Override
- public boolean remove(final Object value) {
- final Iterator<V> it = iterator();
- while (it.hasNext()) {
- if (Objects.equals(it.next(), value)) {
- it.remove();
- return true;
- }
- }
- return false;
- }
+ /**
+ * The current time in milliseconds.
+ */
+ private long now() {
+ return System.currentTimeMillis();
+ }
- @Override
- public boolean removeAll(final Collection<?> coll) {
- Objects.requireNonNull(coll);
- boolean changed = false;
- final Iterator<?> it = iterator();
- while (it.hasNext()) {
- if (coll.contains(it.next())) {
- it.remove();
- changed = true;
- }
- }
- return changed;
- }
+ /**
+ * {@inheritDoc}
+ * <p>
+ * 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
+ * {@link #expiringPolicy}.
+ * </p>
+ */
+ @Override
+ public V put(final K key, final V value) {
+ // remove the previous record
+ removeIfExpired(key, now());
- @Override
- public boolean retainAll(final Collection<?> coll) {
- Objects.requireNonNull(coll);
- boolean changed = false;
- final Iterator<?> it = iterator();
- while (it.hasNext()) {
- if (!coll.contains(it.next())) {
- it.remove();
- changed = true;
- }
- }
- return changed;
- }
+ // record expiration time of new entry
+ final long expirationTime = expiringPolicy.expirationTime(key, value);
+ expirationMap.put(key, Long.valueOf(expirationTime));
- @Override
- public boolean removeIf(final Predicate<? super V> filter) {
- Objects.requireNonNull(filter);
- boolean changed = false;
- final Iterator<V> it = iterator();
- while (it.hasNext()) {
- if (filter.test(it.next())) {
- it.remove();
- changed = true;
- }
- }
- return changed;
- }
+ return super.put(key, value);
+ }
- @Override
- public void clear() {
- PassiveExpiringMap.this.clear();
+ @Override
+ public void putAll(final Map<? extends K, ? extends V> mapToCopy) {
+ for (final Map.Entry<? extends K, ? extends V> entry :
mapToCopy.entrySet()) {
+ put(entry.getKey(), entry.getValue());
}
+ }
- @Override
- public Object[] toArray() {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.toArray();
- }
+ /**
+ * Deserializes the map in using a custom routine.
+ *
+ * @param in the input stream
+ * @throws IOException if an error occurs while reading from the stream
+ * @throws ClassNotFoundException if an object read from the stream cannot
be loaded
+ */
+ @SuppressWarnings("unchecked")
+ // (1) should only fail if input stream is incorrect
+ private void readObject(final ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ map = (Map<K, V>) in.readObject(); // (1)
+ }
- @Override
- public <T> T[] toArray(final T[] array) {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.toArray(array);
- }
+ /**
+ * Normal {@link Map#remove(Object)} behavior with the addition of removing
+ * any expiration entry as well.
+ * {@inheritDoc}
+ */
+ @Override
+ public V remove(final Object key) {
+ expirationMap.remove(key);
+ return super.remove(key);
+ }
- @Override
- public boolean containsAll(final Collection<?> coll) {
-
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
- return super.containsAll(coll);
+ /**
+ * Removes all entries in the map whose expiration time is less than
+ * {@code now}. The exceptions are entries with negative expiration
+ * times; those entries are never removed.
+ *
+ * @see #isExpired(long, Long)
+ */
+ private void removeAllExpired(final long nowMillis) {
+ final Iterator<Map.Entry<Object, Long>> iter =
expirationMap.entrySet().iterator();
+ while (iter.hasNext()) {
+ final Map.Entry<Object, Long> expirationEntry = iter.next();
+ if (isExpired(nowMillis, expirationEntry.getValue())) {
+ // remove entry from collection
+ super.remove(expirationEntry.getKey());
+ // remove entry from expiration map
+ iter.remove();
+ }
}
}
- private final class ValuesIterator implements Iterator<V> {
- private final Iterator<Entry<K, V>> iterator;
-
- private ValuesIterator(final Iterator<Entry<K, V>> iterator) {
- this.iterator = iterator;
+ /**
+ * Removes the entry with the given key if the entry's expiration time is
+ * less than {@code now}. If the entry has a negative expiration time,
+ * the entry is never removed.
+ */
+ private void removeIfExpired(final Object key, final long nowMillis) {
+ final Long expirationTimeObject = expirationMap.get(key);
+ if (isExpired(nowMillis, expirationTimeObject)) {
+ remove(key);
}
+ }
- @Override
- public boolean hasNext() {
- return iterator.hasNext();
- }
+ /**
+ * All expired entries are removed from the map prior to returning the
size.
+ * {@inheritDoc}
+ */
+ @Override
+ public int size() {
+ removeAllExpired(now());
+ return super.size();
+ }
- @Override
- public V next() {
- return iterator.next().getValue();
- }
+ /**
+ * All expired entries are removed from the map prior to returning the
value collection.
+ * {@inheritDoc}
+ */
+ @Override
+ public Collection<V> values() {
+ removeAllExpired(now());
+ return new ValuesCollection(super.values());
+ }
- @Override
- public void remove() {
- iterator.remove();
- }
+ /**
+ * Serializes this object to an ObjectOutputStream.
+ *
+ * @param out the target ObjectOutputStream.
+ * @throws IOException thrown when an I/O errors occur writing to the
target stream.
+ */
+ private void writeObject(final ObjectOutputStream out)
+ throws IOException {
+ out.defaultWriteObject();
+ out.writeObject(map);
}
}
diff --git
a/src/test/java/org/apache/commons/collections4/map/PassiveExpiringMapTest.java
b/src/test/java/org/apache/commons/collections4/map/PassiveExpiringMapTest.java
index 436d6cfdc..e366964bc 100644
---
a/src/test/java/org/apache/commons/collections4/map/PassiveExpiringMapTest.java
+++
b/src/test/java/org/apache/commons/collections4/map/PassiveExpiringMapTest.java
@@ -75,6 +75,17 @@ public class PassiveExpiringMapTest<K, V> extends
AbstractMapTest<PassiveExpirin
return "4";
}
+ @SuppressWarnings("unchecked")
+ private Map<Object, Long> getExpirationMap(final PassiveExpiringMap<?, ?>
map) {
+ try {
+ final java.lang.reflect.Field field =
PassiveExpiringMap.class.getDeclaredField("expirationMap");
+ field.setAccessible(true);
+ return (Map<Object, Long>) field.get(map);
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
@Override
protected int getIterationBehaviour() {
return AbstractCollectionTest.UNORDERED;
@@ -108,6 +119,104 @@ public class PassiveExpiringMapTest<K, V> extends
AbstractMapTest<PassiveExpirin
return m;
}
+ @Test
+ void testCollectionsSynchronizedMapExpiration() throws
InterruptedException {
+ final Map<String, String> map = Collections.synchronizedMap(new
PassiveExpiringMap<>(50L));
+ map.put("a", "b");
+ map.put("c", "d");
+ assertEquals(2, map.size());
+ // Cache the views in SynchronizedMap before they expire
+ final Collection<Map.Entry<String, String>> entrySet = map.entrySet();
+ final Collection<String> keySet = map.keySet();
+ final Collection<String> values = map.values();
+ Thread.sleep(100L);
+ // entrySet view access triggers expiration
+ synchronized (map) {
+ assertTrue(entrySet.isEmpty());
+ assertTrue(keySet.isEmpty());
+ assertTrue(values.isEmpty());
+ }
+ map.put("a", "b");
+ map.put("c", "d");
+ assertEquals(2, map.size());
+ Thread.sleep(100L);
+ // keySet view access triggers expiration
+ synchronized (map) {
+ assertTrue(entrySet.isEmpty());
+ assertTrue(keySet.isEmpty());
+ assertTrue(values.isEmpty());
+ }
+ map.put("a", "b");
+ map.put("c", "d");
+ assertEquals(2, map.size());
+ Thread.sleep(100L);
+ // values view access triggers expiration
+ synchronized (map) {
+ assertTrue(entrySet.isEmpty());
+ assertTrue(keySet.isEmpty());
+ assertTrue(values.isEmpty());
+ }
+ }
+
+ @Test
+ void testCollectionViewIteratorExpiration() throws InterruptedException {
+ final PassiveExpiringMap<String, String> map = new
PassiveExpiringMap<>(50L);
+ map.put("a", "b");
+
+ final Collection<Map.Entry<String, String>> entrySet = map.entrySet();
+ final Collection<String> keySet = map.keySet();
+ final Collection<String> values = map.values();
+
+ Thread.sleep(100L);
+
+ // The iterators should trigger expiration and not return any elements
+ assertFalse(entrySet.iterator().hasNext());
+ assertFalse(keySet.iterator().hasNext());
+ assertFalse(values.iterator().hasNext());
+ }
+
+ @Test
+ void testCollectionViewNullInputs() {
+ final PassiveExpiringMap<String, String> map = new
PassiveExpiringMap<>(10000L);
+ map.put("a", "b");
+ // entrySet
+ assertThrows(NullPointerException.class, () ->
map.entrySet().removeAll(null));
+ assertThrows(NullPointerException.class, () ->
map.entrySet().retainAll(null));
+ // keySet
+ assertThrows(NullPointerException.class, () ->
map.keySet().removeAll(null));
+ assertThrows(NullPointerException.class, () ->
map.keySet().retainAll(null));
+ // values
+ assertThrows(NullPointerException.class, () ->
map.values().removeAll(null));
+ assertThrows(NullPointerException.class, () ->
map.values().retainAll(null));
+ }
+
+ @Test
+ void testCollectionViewRemoval() {
+ final PassiveExpiringMap<String, String> map = new
PassiveExpiringMap<>(10000L);
+ map.put("a", "b");
+ map.put("c", "d");
+ map.put("e", "f");
+ // Remove via entrySet iterator
+ final Iterator<Map.Entry<String, String>> entryIter =
map.entrySet().iterator();
+ assertTrue(entryIter.hasNext());
+ final Map.Entry<String, String> entry = entryIter.next();
+ final String removedKey = entry.getKey();
+ entryIter.remove();
+ assertFalse(map.containsKey(removedKey));
+ // Remove via keySet iterator
+ final Iterator<String> keyIter = map.keySet().iterator();
+ assertTrue(keyIter.hasNext());
+ final String key = keyIter.next();
+ keyIter.remove();
+ assertFalse(map.containsKey(key));
+ // Remove via values iterator
+ final Iterator<String> valIter = map.values().iterator();
+ assertTrue(valIter.hasNext());
+ final String val = valIter.next();
+ valIter.remove();
+ assertFalse(map.containsValue(val));
+ }
+
@Test
void testConstructors() {
assertThrows(NullPointerException.class, () -> {
@@ -190,166 +299,6 @@ public class PassiveExpiringMapTest<K, V> extends
AbstractMapTest<PassiveExpirin
validateExpiration(new PassiveExpiringMap<>(new
PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<>(1, TimeUnit.SECONDS)),
1000);
}
- @Test
- void testGet() {
- final Map<Integer, String> m = makeTestMap();
- assertNull(m.get(Integer.valueOf(1)));
- assertEquals("two", m.get(Integer.valueOf(2)));
- assertNull(m.get(Integer.valueOf(3)));
- assertEquals("four", m.get(Integer.valueOf(4)));
- assertNull(m.get(Integer.valueOf(5)));
- assertEquals("six", m.get(Integer.valueOf(6)));
- }
-
- @Test
- void testIsEmpty() {
- Map<Integer, String> m = makeTestMap();
- assertFalse(m.isEmpty());
-
- // remove just evens
- m = makeTestMap();
- m.remove(Integer.valueOf(2));
- m.remove(Integer.valueOf(4));
- m.remove(Integer.valueOf(6));
- assertTrue(m.isEmpty());
- }
-
- @Test
- void testKeySet() {
- final Map<Integer, String> m = makeTestMap();
- assertEquals(3, m.size());
- }
-
- @Test
- void testPut() {
- final Map<Integer, String> m = makeTestMap();
- assertNull(m.put(Integer.valueOf(1), "ONE"));
- assertEquals("two", m.put(Integer.valueOf(2), "TWO"));
- assertNull(m.put(Integer.valueOf(3), "THREE"));
- assertEquals("four", m.put(Integer.valueOf(4), "FOUR"));
- assertNull(m.put(Integer.valueOf(5), "FIVE"));
- assertEquals("six", m.put(Integer.valueOf(6), "SIX"));
- }
-
- @Test
- void testSize() {
- final Map<Integer, String> m = makeTestMap();
- assertEquals(3, m.size());
- }
-
- @Test
- void testValues() {
- final Map<Integer, String> m = makeTestMap();
- assertEquals(3, m.size());
- }
-
- @Test
- void testZeroTimeToLive() {
- // item should not be available
- final PassiveExpiringMap<String, String> m = new
PassiveExpiringMap<>(0L);
- m.put("a", "b");
- assertNull(m.get("a"));
- }
-
- private void validateExpiration(final Map<String, String> map, final long
timeout) throws InterruptedException {
- map.put("a", "b");
- assertNotNull(map.get("a"));
- Thread.sleep(2 * timeout);
- assertNull(map.get("a"));
- }
-
- @Test
- void testCollectionsSynchronizedMapExpiration() throws
InterruptedException {
- final Map<String, String> map = Collections.synchronizedMap(new
PassiveExpiringMap<>(50L));
- map.put("a", "b");
- map.put("c", "d");
- assertEquals(2, map.size());
- // Cache the views in SynchronizedMap before they expire
- final Collection<Map.Entry<String, String>> entrySet = map.entrySet();
- final Collection<String> keySet = map.keySet();
- final Collection<String> values = map.values();
- Thread.sleep(100L);
- // entrySet view access triggers expiration
- synchronized (map) {
- assertTrue(entrySet.isEmpty());
- assertTrue(keySet.isEmpty());
- assertTrue(values.isEmpty());
- }
- map.put("a", "b");
- map.put("c", "d");
- assertEquals(2, map.size());
- Thread.sleep(100L);
- // keySet view access triggers expiration
- synchronized (map) {
- assertTrue(entrySet.isEmpty());
- assertTrue(keySet.isEmpty());
- assertTrue(values.isEmpty());
- }
- map.put("a", "b");
- map.put("c", "d");
- assertEquals(2, map.size());
- Thread.sleep(100L);
- // values view access triggers expiration
- synchronized (map) {
- assertTrue(entrySet.isEmpty());
- assertTrue(keySet.isEmpty());
- assertTrue(values.isEmpty());
- }
- }
-
- @Test
- void testCollectionViewRemoval() {
- final PassiveExpiringMap<String, String> map = new
PassiveExpiringMap<>(10000L);
- map.put("a", "b");
- map.put("c", "d");
- map.put("e", "f");
- // Remove via entrySet iterator
- final Iterator<Map.Entry<String, String>> entryIter =
map.entrySet().iterator();
- assertTrue(entryIter.hasNext());
- final Map.Entry<String, String> entry = entryIter.next();
- final String removedKey = entry.getKey();
- entryIter.remove();
- assertFalse(map.containsKey(removedKey));
- // Remove via keySet iterator
- final Iterator<String> keyIter = map.keySet().iterator();
- assertTrue(keyIter.hasNext());
- final String key = keyIter.next();
- keyIter.remove();
- assertFalse(map.containsKey(key));
- // Remove via values iterator
- final Iterator<String> valIter = map.values().iterator();
- assertTrue(valIter.hasNext());
- final String val = valIter.next();
- valIter.remove();
- assertFalse(map.containsValue(val));
- }
-
- @Test
- void testCollectionViewNullInputs() {
- final PassiveExpiringMap<String, String> map = new
PassiveExpiringMap<>(10000L);
- map.put("a", "b");
- // entrySet
- assertThrows(NullPointerException.class, () ->
map.entrySet().removeAll(null));
- assertThrows(NullPointerException.class, () ->
map.entrySet().retainAll(null));
- // keySet
- assertThrows(NullPointerException.class, () ->
map.keySet().removeAll(null));
- assertThrows(NullPointerException.class, () ->
map.keySet().retainAll(null));
- // values
- assertThrows(NullPointerException.class, () ->
map.values().removeAll(null));
- assertThrows(NullPointerException.class, () ->
map.values().retainAll(null));
- }
-
- @SuppressWarnings("unchecked")
- private Map<Object, Long> getExpirationMap(final PassiveExpiringMap<?, ?>
map) {
- try {
- final java.lang.reflect.Field field =
PassiveExpiringMap.class.getDeclaredField("expirationMap");
- field.setAccessible(true);
- return (Map<Object, Long>) field.get(map);
- } catch (final Exception e) {
- throw new RuntimeException(e);
- }
- }
-
@Test
void testExpirationMapCleanup() {
final PassiveExpiringMap<String, String> map = new
PassiveExpiringMap<>(10000L);
@@ -423,20 +372,71 @@ public class PassiveExpiringMapTest<K, V> extends
AbstractMapTest<PassiveExpirin
}
@Test
- void testCollectionViewIteratorExpiration() throws InterruptedException {
- final PassiveExpiringMap<String, String> map = new
PassiveExpiringMap<>(50L);
- map.put("a", "b");
+ void testGet() {
+ final Map<Integer, String> m = makeTestMap();
+ assertNull(m.get(Integer.valueOf(1)));
+ assertEquals("two", m.get(Integer.valueOf(2)));
+ assertNull(m.get(Integer.valueOf(3)));
+ assertEquals("four", m.get(Integer.valueOf(4)));
+ assertNull(m.get(Integer.valueOf(5)));
+ assertEquals("six", m.get(Integer.valueOf(6)));
+ }
- final Collection<Map.Entry<String, String>> entrySet = map.entrySet();
- final Collection<String> keySet = map.keySet();
- final Collection<String> values = map.values();
+ @Test
+ void testIsEmpty() {
+ Map<Integer, String> m = makeTestMap();
+ assertFalse(m.isEmpty());
- Thread.sleep(100L);
+ // remove just evens
+ m = makeTestMap();
+ m.remove(Integer.valueOf(2));
+ m.remove(Integer.valueOf(4));
+ m.remove(Integer.valueOf(6));
+ assertTrue(m.isEmpty());
+ }
- // The iterators should trigger expiration and not return any elements
- assertFalse(entrySet.iterator().hasNext());
- assertFalse(keySet.iterator().hasNext());
- assertFalse(values.iterator().hasNext());
+ @Test
+ void testKeySet() {
+ final Map<Integer, String> m = makeTestMap();
+ assertEquals(3, m.size());
+ }
+
+ @Test
+ void testPut() {
+ final Map<Integer, String> m = makeTestMap();
+ assertNull(m.put(Integer.valueOf(1), "ONE"));
+ assertEquals("two", m.put(Integer.valueOf(2), "TWO"));
+ assertNull(m.put(Integer.valueOf(3), "THREE"));
+ assertEquals("four", m.put(Integer.valueOf(4), "FOUR"));
+ assertNull(m.put(Integer.valueOf(5), "FIVE"));
+ assertEquals("six", m.put(Integer.valueOf(6), "SIX"));
+ }
+
+ @Test
+ void testSize() {
+ final Map<Integer, String> m = makeTestMap();
+ assertEquals(3, m.size());
+ }
+
+ @Test
+ void testValues() {
+ final Map<Integer, String> m = makeTestMap();
+ assertEquals(3, m.size());
+ }
+
+ @Test
+ void testZeroTimeToLive() {
+ // item should not be available
+ final PassiveExpiringMap<String, String> m = new
PassiveExpiringMap<>(0L);
+ m.put("a", "b");
+ assertNull(m.get("a"));
+ }
+
+ private void validateExpiration(final Map<String, String> map, final long
timeout) throws InterruptedException {
+ map.put("a", "b");
+ assertNotNull(map.get("a"));
+ Thread.sleep(2 * timeout);
+ assertNull(map.get("a"));
}
}