Create Duration class and ExpiringSet#getExpiration (prep for #1073)
- Move expiring collections to util.expiring package - Change ExpiringSet to remove expired entries during normal calls
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package fr.xephi.authme.util.expiring;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Represents a duration in time, defined by a time unit and a duration.
|
||||
*/
|
||||
public class Duration {
|
||||
|
||||
private final long duration;
|
||||
private final TimeUnit unit;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param duration the duration
|
||||
* @param unit the time unit in which {@code duration} is expressed
|
||||
*/
|
||||
public Duration(long duration, TimeUnit unit) {
|
||||
this.duration = duration;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Duration object for the given duration and unit in the most suitable time unit.
|
||||
* For example, {@code createWithSuitableUnit(120, TimeUnit.SECONDS)} will return a Duration
|
||||
* object of 2 minutes.
|
||||
* <p>
|
||||
* This method only considers the time units days, hours, minutes, and seconds for the objects
|
||||
* it creates. Conversion is done with {@link TimeUnit#convert} and so always rounds the
|
||||
* results down.
|
||||
* <p>
|
||||
* Further examples:
|
||||
* <code>createWithSuitableUnit(299, TimeUnit.MINUTES); // 4 hours</code>
|
||||
* <code>createWithSuitableUnit(700, TimeUnit.MILLISECONDS); // 0 seconds</code>
|
||||
*
|
||||
* @param sourceDuration the duration
|
||||
* @param sourceUnit the time unit the duration is expressed in
|
||||
* @return Duration object using the most suitable time unit
|
||||
*/
|
||||
public static Duration createWithSuitableUnit(long sourceDuration, TimeUnit sourceUnit) {
|
||||
long durationMillis = Math.abs(TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit));
|
||||
|
||||
TimeUnit targetUnit;
|
||||
if (durationMillis > 1000L * 60L * 60L * 24L) {
|
||||
targetUnit = TimeUnit.DAYS;
|
||||
} else if (durationMillis > 1000L * 60L * 60L) {
|
||||
targetUnit = TimeUnit.HOURS;
|
||||
} else if (durationMillis > 1000L * 60L) {
|
||||
targetUnit = TimeUnit.MINUTES;
|
||||
} else {
|
||||
targetUnit = TimeUnit.SECONDS;
|
||||
}
|
||||
|
||||
long durationInTargetUnit = targetUnit.convert(sourceDuration, sourceUnit);
|
||||
return new Duration(durationInTargetUnit, targetUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the duration
|
||||
*/
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the time unit in which the duration is expressed
|
||||
*/
|
||||
public TimeUnit getTimeUnit() {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package fr.xephi.authme.util.expiring;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Map with expiring entries. Following a configured amount of time after
|
||||
* an entry has been inserted, the map will act as if the entry does not
|
||||
* exist.
|
||||
* <p>
|
||||
* Time starts counting directly after insertion. Inserting a new entry with
|
||||
* a key that already has a value will "reset" the expiration. Although the
|
||||
* expiration can be redefined later on, only entries which are inserted
|
||||
* afterwards will use the new expiration.
|
||||
* <p>
|
||||
* An expiration of {@code <= 0} will make the map expire all entries
|
||||
* immediately after insertion. Note that the map does not remove expired
|
||||
* entries automatically; this is only done when calling
|
||||
* {@link #removeExpiredEntries()}.
|
||||
*
|
||||
* @param <K> the key type
|
||||
* @param <V> the value type
|
||||
*/
|
||||
public class ExpiringMap<K, V> {
|
||||
|
||||
protected final Map<K, ExpiringEntry<V>> entries = new ConcurrentHashMap<>();
|
||||
private long expirationMillis;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param duration the duration of time after which entries expire
|
||||
* @param unit the time unit in which {@code duration} is expressed
|
||||
*/
|
||||
public ExpiringMap(long duration, TimeUnit unit) {
|
||||
setExpiration(duration, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value associated with the given key,
|
||||
* if available and not expired.
|
||||
*
|
||||
* @param key the key to look up
|
||||
* @return the associated value, or {@code null} if not available
|
||||
*/
|
||||
public V get(K key) {
|
||||
ExpiringEntry<V> value = entries.get(key);
|
||||
return value == null ? null : value.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a value for the given key. Overwrites a previous value
|
||||
* for the key if it exists.
|
||||
*
|
||||
* @param key the key to insert a value for
|
||||
* @param value the value to insert
|
||||
*/
|
||||
public void put(K key, V value) {
|
||||
long expiration = System.currentTimeMillis() + expirationMillis;
|
||||
entries.put(key, new ExpiringEntry<>(value, expiration));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the value for the given key, if available.
|
||||
*
|
||||
* @param key the key to remove the value for
|
||||
*/
|
||||
public void remove(K key) {
|
||||
entries.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries which have expired from the internal structure.
|
||||
*/
|
||||
public void removeExpiredEntries() {
|
||||
entries.entrySet().removeIf(entry -> System.currentTimeMillis() > entry.getValue().getExpiration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new expiration duration. Note that already present entries
|
||||
* will still make use of the old expiration.
|
||||
*
|
||||
* @param duration the duration of time after which entries expire
|
||||
* @param unit the time unit in which {@code duration} is expressed
|
||||
*/
|
||||
public void setExpiration(long duration, TimeUnit unit) {
|
||||
this.expirationMillis = unit.toMillis(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this map is empty. This reflects the state of the
|
||||
* internal map, which may contain expired entries only. The result
|
||||
* may change after running {@link #removeExpiredEntries()}.
|
||||
*
|
||||
* @return true if map is really empty, false otherwise
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return entries.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Class holding a value paired with an expiration timestamp.
|
||||
*
|
||||
* @param <V> the value type
|
||||
*/
|
||||
protected static final class ExpiringEntry<V> {
|
||||
|
||||
private final V value;
|
||||
private final long expiration;
|
||||
|
||||
ExpiringEntry(V value, long expiration) {
|
||||
this.value = value;
|
||||
this.expiration = expiration;
|
||||
}
|
||||
|
||||
V getValue() {
|
||||
return System.currentTimeMillis() > expiration ? null : value;
|
||||
}
|
||||
|
||||
long getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package fr.xephi.authme.util.expiring;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Set whose entries expire after a configurable amount of time. Once an entry
|
||||
* has expired, the set will act as if the entry no longer exists. Time starts
|
||||
* counting after the entry has been inserted.
|
||||
* <p>
|
||||
* Internally, expired entries are not guaranteed to be cleared automatically.
|
||||
* A cleanup of all expired entries may be triggered with
|
||||
* {@link #removeExpiredEntries()}. Adding an entry that is already present
|
||||
* effectively resets its expiration.
|
||||
*
|
||||
* @param <E> the type of the entries
|
||||
*/
|
||||
public class ExpiringSet<E> {
|
||||
|
||||
private Map<E, Long> entries = new ConcurrentHashMap<>();
|
||||
private long expirationMillis;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param duration the duration of time after which entries expire
|
||||
* @param unit the time unit in which {@code duration} is expressed
|
||||
*/
|
||||
public ExpiringSet(long duration, TimeUnit unit) {
|
||||
setExpiration(duration, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entry to the set.
|
||||
*
|
||||
* @param entry the entry to add
|
||||
*/
|
||||
public void add(E entry) {
|
||||
entries.put(entry, System.currentTimeMillis() + expirationMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this set contains the given entry, if it hasn't expired.
|
||||
*
|
||||
* @param entry the entry to check
|
||||
* @return true if the entry is present and not expired, false otherwise
|
||||
*/
|
||||
public boolean contains(E entry) {
|
||||
Long expiration = entries.get(entry);
|
||||
if (expiration == null) {
|
||||
return false;
|
||||
} else if (expiration > System.currentTimeMillis()) {
|
||||
return true;
|
||||
} else {
|
||||
entries.remove(entry);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given entry from the set (if present).
|
||||
*
|
||||
* @param entry the entry to remove
|
||||
*/
|
||||
public void remove(E entry) {
|
||||
entries.remove(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries from the set.
|
||||
*/
|
||||
public void clear() {
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all entries which have expired from the internal structure.
|
||||
*/
|
||||
public void removeExpiredEntries() {
|
||||
entries.entrySet().removeIf(entry -> System.currentTimeMillis() > entry.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration of the entry until it expires (provided it is not removed or re-added).
|
||||
* If the entry does not exist, -1 is returned.
|
||||
*
|
||||
* @param entry the entry whose duration before it expires should be returned
|
||||
* @param unit the unit in which to return the duration
|
||||
* @return duration the entry will remain in the set (if there are not modifications)
|
||||
*/
|
||||
public long getExpiration(E entry, TimeUnit unit) {
|
||||
Long expiration = entries.get(entry);
|
||||
if (expiration == null) {
|
||||
return -1;
|
||||
}
|
||||
long stillPresentMillis = expiration - System.currentTimeMillis();
|
||||
if (stillPresentMillis < 0) {
|
||||
entries.remove(entry);
|
||||
return -1;
|
||||
}
|
||||
return unit.convert(stillPresentMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new expiration duration. Note that already present entries
|
||||
* will still make use of the old expiration.
|
||||
*
|
||||
* @param duration the duration of time after which entries expire
|
||||
* @param unit the time unit in which {@code duration} is expressed
|
||||
*/
|
||||
public void setExpiration(long duration, TimeUnit unit) {
|
||||
this.expirationMillis = unit.toMillis(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this map is empty. This reflects the state of the
|
||||
* internal map, which may contain expired entries only. The result
|
||||
* may change after running {@link #removeExpiredEntries()}.
|
||||
*
|
||||
* @return true if map is really empty, false otherwise
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return entries.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package fr.xephi.authme.util.expiring;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Keeps a count per key which expires after a configurable amount of time.
|
||||
* <p>
|
||||
* Once the expiration of an entry has been reached, the counter resets
|
||||
* to 0. The counter returns 0 rather than {@code null} for any given key.
|
||||
*/
|
||||
public class TimedCounter<K> extends ExpiringMap<K, Integer> {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param duration the duration of time after which entries expire
|
||||
* @param unit the time unit in which {@code duration} is expressed
|
||||
*/
|
||||
public TimedCounter(long duration, TimeUnit unit) {
|
||||
super(duration, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer get(K key) {
|
||||
Integer value = super.get(key);
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the value stored for the provided key.
|
||||
*
|
||||
* @param key the key to increment the counter for
|
||||
*/
|
||||
public void increment(K key) {
|
||||
put(key, get(key) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total of all non-expired entries in this counter.
|
||||
*
|
||||
* @return the total of all valid entries
|
||||
*/
|
||||
public int total() {
|
||||
return entries.values().stream()
|
||||
.map(ExpiringEntry::getValue)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(0, Integer::sum);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user