/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.jcwhatever.nucleus.collections.timed; import com.jcwhatever.nucleus.Nucleus; import com.jcwhatever.nucleus.collections.wrap.ConversionEntryWrapper; import com.jcwhatever.nucleus.collections.wrap.ConversionIteratorWrapper; import com.jcwhatever.nucleus.collections.wrap.IteratorWrapper; import com.jcwhatever.nucleus.collections.wrap.SetWrapper; import com.jcwhatever.nucleus.collections.wrap.SyncStrategy; import com.jcwhatever.nucleus.mixins.IPluginOwned; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.Rand; import com.jcwhatever.nucleus.managed.scheduler.Scheduler; import com.jcwhatever.nucleus.utils.TimeScale; import com.jcwhatever.nucleus.utils.observer.update.IUpdateSubscriber; import com.jcwhatever.nucleus.utils.observer.update.NamedUpdateAgents; import com.jcwhatever.nucleus.managed.scheduler.IScheduledTask; import com.jcwhatever.nucleus.utils.performance.pool.IPoolElementFactory; import com.jcwhatever.nucleus.utils.performance.pool.SimpleConcurrentPool; import org.bukkit.plugin.Plugin; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import javax.annotation.Nullable; /** * An encapsulated {@link HashMap} where each key value has an individual lifespan that * when ended, causes the item to be removed. * * <p>The lifespan can be reset by re-adding an item.</p> * * <p>Items can be added using the default lifespan time or a lifespan can be specified per item.</p> * * <p>Subscribers that are added to track when an item expires or the collection is * empty will have a varying degree of resolution up to 10 ticks, meaning the subscriber * may be notified up to 10 ticks after an element expires (but not before).</p> * * <p>Getter operations cease to return an element within approximately 50 milliseconds * (1 tick) of expiring.</p> * * <p>Thread Safe.</p> * * <p>The maps iterators must be used inside a synchronized block which locks the * map instance. Otherwise, a {@link java.lang.IllegalStateException} is thrown.</p> */ public class TimedHashMap<K, V> implements Map<K, V>, IPluginOwned { // The minimum interval the cleanup is allowed to run at. // Used to prevent cleanup from being run too often. private static final int MIN_CLEANUP_INTERVAL_MS = 50; // The interval the janitor runs at private static final int JANITOR_INTERVAL_TICKS = 10; // random initial delay interval for janitor, random to help spread out // task execution in relation to other scheduled tasks private static final int JANITOR_INITIAL_DELAY_TICKS = Rand.getInt(1, 5); private final static Map<TimedHashMap, Void> _instances = new WeakHashMap<>(10); private static IScheduledTask _janitor; private final Plugin _plugin; private final Map<K, DateEntry<K, V>> _map; private final int _lifespan; // milliseconds private final TimeScale _timeScale; private final transient ValuesWrapper _valuesWrapper; private final transient KeySetWrapper _keySetWrapper; private final transient EntrySetWrapper _entrySetWrapper; private final transient Object _sync; private final transient SyncStrategy _strategy; private transient long _nextCleanup; private final transient NamedUpdateAgents _agents = new NamedUpdateAgents(); private final transient SimpleConcurrentPool<DateEntry> _entryPool; /** * Constructor. * * <p>Default lifespan is 20 ticks.</p> * * <p>The initial capacity is 10.</p> */ public TimedHashMap(Plugin plugin) { this(plugin, 10, 20, TimeScale.TICKS); } /** * Constructor. * * <p>Default lifespan is 20 ticks.</p> * * @param capacity The initial capacity of the map. */ public TimedHashMap(Plugin plugin, int capacity) { this(plugin, capacity, 20, TimeScale.TICKS); } /** * Constructor. * * @param capacity The initial capacity of the map. * @param defaultLifespan The default lifespan of items in ticks. * @param timeScale The lifespan time scale. */ public TimedHashMap(Plugin plugin, int capacity, int defaultLifespan, TimeScale timeScale) { PreCon.notNull(plugin); PreCon.positiveNumber(defaultLifespan); PreCon.notNull(timeScale); _plugin = plugin; _sync = this; _strategy = new SyncStrategy(this); _lifespan = defaultLifespan * timeScale.getTimeFactor(); _timeScale = timeScale; _map = new HashMap<>(capacity); _valuesWrapper = new ValuesWrapper(); _keySetWrapper = new KeySetWrapper(); _entrySetWrapper = new EntrySetWrapper(); _entryPool = new SimpleConcurrentPool<DateEntry>(capacity, new IPoolElementFactory<DateEntry>() { @Override public DateEntry create() { return new DateEntry<>(TimedHashMap.this); } }); synchronized (_instances) { _instances.put(this, null); } startJanitor(); } /** * Put an item into the map using the specified lifespan in * the time scale specified in the constructor. * * @param key The item key. * @param value The item to add. * @param lifespan The items lifespan. */ public V put(final K key, final V value, int lifespan) { return put(key, value, lifespan, _timeScale); } /** * Put an item into the map using the specified lifespan in * the time scale specified. * * @param key The item key. * @param value The item to add. * @param lifespan The items lifespan. * @param timeScale The time scale of the specified lifespan. */ public V put(final K key, final V value, int lifespan, TimeScale timeScale) { PreCon.notNull(key); PreCon.notNull(value); PreCon.positiveNumber(lifespan); PreCon.notNull(timeScale); DateEntry<K, V> previous; synchronized (_sync) { @SuppressWarnings("unchecked") DateEntry<K, V> entry = (DateEntry<K, V>)_entryPool.retrieve(); assert entry != null; previous = _map.put(key, entry.asEntry(key, value, lifespan, timeScale)); } if (previous == null) return null; return previous.value; } /** * Put a map of items into the map using the specified lifespan in * the time scale specified in the constructor. * * @param entries The map to add. * @param lifespan The lifespan of the added items. */ public void putAll(Map<? extends K, ? extends V> entries, int lifespan) { putAll(entries, lifespan, _timeScale); } /** * Put a map of items into the map using the specified lifespan in * the time scale specified.. * * @param entries The map to add. * @param lifespan The lifespan of the added items. * @param timeScale The timeScale of the specified lifespan. */ public void putAll(Map<? extends K, ? extends V> entries, int lifespan, TimeScale timeScale) { PreCon.notNull(entries); PreCon.positiveNumber(lifespan); PreCon.notNull(timeScale); synchronized (_sync) { for (Map.Entry<? extends K, ? extends V> entry : entries.entrySet()) { @SuppressWarnings("unchecked") DateEntry<K, V> dateEntry = (DateEntry<K, V>)_entryPool.retrieve(); assert dateEntry != null; _map.put(entry.getKey(), dateEntry.asEntry( entry.getKey(), entry.getValue(), lifespan, timeScale)); } } } /** * Set the maximum size of the internal object pool used * for pooling internal instances. * * @param poolSize The maximum pool size. -1 for "infinite". * * @return Self for chaining. */ public TimedHashMap<K, V> setMaxPoolSize(int poolSize) { _entryPool.setMaxSize(poolSize); return this; } /** * Get the maximum size of the internal object pool used * for pooling internal instances. */ public int getMaxPoolSize() { return _entryPool.maxSize(); } /** * Register a subscriber to be notified whenever an entry is removed * due to its lifespan ending. * * @param subscriber The subscriber. * * @return Self for chaining. */ public TimedHashMap<K, V> onLifespanEnd(IUpdateSubscriber<Entry<K, V>> subscriber) { PreCon.notNull(subscriber); _agents.getAgent("onLifespanEnd").addSubscriber(subscriber); return this; } /** * Register a subscriber to be notified whenever the map becomes * empty due to an entries lifespan ending. * * @param subscriber The subscriber. * * @return Self for chaining. */ public TimedHashMap<K, V> onEmpty(IUpdateSubscriber<TimedHashMap<K, V>> subscriber) { PreCon.notNull(subscriber); _agents.getAgent("onEmpty").addSubscriber(subscriber); return this; } @Override public Plugin getPlugin() { return _plugin; } @Override public void clear() { synchronized (_sync) { for (Entry<K, DateEntry<K, V>> entry : _map.entrySet()) { entry.getValue().recycle(); } _map.clear(); } } @Override public Set<K> keySet() { return _keySetWrapper; } @Override public Collection<V> values() { return _valuesWrapper; } @Override public Set<Entry<K, V>> entrySet() { return _entrySetWrapper; } @Override public int size() { synchronized (_sync) { cleanup(); return _map.size(); } } @Override public boolean isEmpty() { synchronized (_sync) { cleanup(); return _map.isEmpty(); } } @Override public boolean containsKey(Object key) { PreCon.notNull(key); synchronized (_sync) { //noinspection SuspiciousMethodCalls DateEntry<K, V> entry = _map.get(key); if (entry == null) return false; if (entry.isExpired()) { //noinspection SuspiciousMethodCalls _map.remove(key); onLifespanEnd(getEntry(entry.key, entry.value)); entry.recycle(); return false; } return true; } } @Override public boolean containsValue(Object value) { PreCon.notNull(value); synchronized (_sync) { cleanup(); return _map.containsValue(value); } } @Override @Nullable public V get(Object key) { PreCon.notNull(key); synchronized (_sync) { DateEntry<K, V> entry = _map.get(key); if (entry == null) return null; if (entry.isExpired()) { //noinspection SuspiciousMethodCalls _map.remove(key); onLifespanEnd(getEntry(entry.key, entry.value)); } else { return entry.value; } } return null; } @Override @Nullable public V put(K key, V value) { PreCon.notNull(key); PreCon.notNull(value); return put(key, value, _lifespan, TimeScale.MILLISECONDS); } @Override public void putAll(Map<? extends K, ? extends V> entries) { PreCon.notNull(entries); putAll(entries, _lifespan, TimeScale.MILLISECONDS); } @Override @Nullable public V remove(Object key) { PreCon.notNull(key); synchronized (_sync) { DateEntry<K, V> value = _map.remove(key); if (value == null) return null; V result = value.value; value.recycle(); return result; } } private void onLifespanEnd(Entry<K, V> value) { if (!_agents.hasAgent("onLifespanEnd")) return; _agents.update("onLifespanEnd", value); if (_map.isEmpty()) _agents.update("onEmpty", this); } private void cleanup() { if (_map.isEmpty()) return; // prevent cleanup from running too often if (_nextCleanup > System.currentTimeMillis()) return; _nextCleanup = System.currentTimeMillis() + MIN_CLEANUP_INTERVAL_MS; Iterator<Entry<K, DateEntry<K, V>>> iterator = _map.entrySet().iterator(); while (iterator.hasNext()) { Entry<K, DateEntry<K, V>> entry = iterator.next(); if (entry.getValue().isExpired()) { iterator.remove(); onLifespanEnd(getEntry(entry.getKey(), entry.getValue().value)); entry.getValue().recycle(); } } } private void startJanitor() { if (_janitor != null) return; _janitor = Scheduler.runTaskRepeatAsync( Nucleus.getPlugin(), JANITOR_INITIAL_DELAY_TICKS, JANITOR_INTERVAL_TICKS, new Runnable() { @Override public void run() { List<TimedHashMap> maps; synchronized (_instances) { maps = new ArrayList<TimedHashMap>(_instances.keySet()); } for (TimedHashMap map : maps) { // remove from instances if owning plugin is disabled if (!map._plugin.isEnabled()) { synchronized (_instances) { _instances.remove(map); } continue; } // run cleanup synchronized (map._sync) { map.cleanup(); } } } }); } private Entry<K, V> getEntry(final K k, final V v) { return new Entry<K, V>() { V value = v; @Override public K getKey() { return k; } @Override public V getValue() { return value; } @Override public V setValue(V value) { V prev = this.value; this.value = prev; return prev; } }; } private Entry<K, V> getEntry(final Entry<K, DateEntry<K, V>> entry) { return new ConversionEntryWrapper<K, V, DateEntry<K, V>>(_strategy) { @Override protected Entry<K, DateEntry<K, V>> entry() { return entry; } @Override protected V convert(K key, DateEntry<K, V> internal) { return internal.value; } @Override protected DateEntry<K, V> unconvert(K key, V external) { @SuppressWarnings("unchecked") DateEntry<K, V> dateEntry = (DateEntry<K, V>)_entryPool.retrieve(); assert dateEntry != null; return dateEntry.asEntry(key, external, _lifespan, TimeScale.MILLISECONDS); } }; } private static final class DateEntry<K, V> { TimedHashMap<K, V> parent; K key; V value; long expires; Object match; boolean isRecycled; DateEntry(TimedHashMap<K, V> parent) { this.parent = parent; } DateEntry<K, V> asEntry(K key,V value, long lifespan, TimeScale timeScale) { this.key = key; this.value = value; this.expires = System.currentTimeMillis() + (lifespan * timeScale.getTimeFactor()); this.match = value; this.isRecycled = false; return this; } DateEntry<K, V> asMatcher(Object match) { this.key = null; this.match = match; this.expires = 0; this.value = null; this.isRecycled = false; return this; } void recycle() { if (this.isRecycled) return; this.isRecycled = true; this.key = null; this.value = null; this.match = null; parent._entryPool.recycle(this); } boolean isExpired() { return System.currentTimeMillis() >= expires; } @Override public int hashCode() { return match != null ? match.hashCode() : 0; } @Override public boolean equals(Object obj) { return obj == null ? match == null : match != null && (obj instanceof DateEntry ? ((DateEntry) obj).match.equals(match) : obj.equals(match)); } } private final class KeySetWrapper extends SetWrapper<K> { KeySetWrapper() { super(TimedHashMap.this._strategy); } @Override protected boolean onPreAdd(K e) { throw new UnsupportedOperationException(); } @Override protected Set<K> set() { return _map.keySet(); } } private final class ValuesWrapper implements Collection<V> { @Override public int size() { return TimedHashMap.this.size(); } @Override public boolean isEmpty() { return TimedHashMap.this.isEmpty(); } @Override public boolean contains(Object o) { synchronized (_sync) { @SuppressWarnings("unchecked") DateEntry<K, V> dateEntry = (DateEntry<K, V>)_entryPool.retrieve(); assert dateEntry != null; boolean result = _map.values().contains(dateEntry.asMatcher(o)); dateEntry.recycle(); return result; } } @Override public Iterator<V> iterator() { return new Iterator<V>() { Iterator<DateEntry<K, V>> iterator = _map.values().iterator(); @Override public boolean hasNext() { IteratorWrapper.assertIteratorLock(_sync); return iterator.hasNext(); } @Override public V next() { IteratorWrapper.assertIteratorLock(_sync); return iterator.next().value; } @Override public void remove() { IteratorWrapper.assertIteratorLock(_sync); iterator.remove(); } }; } @Override public Object[] toArray() { Object[] array; synchronized (_sync) { array = _map.values().toArray(); } Object[] output = new Object[array.length]; for (int i=0; i < array.length; i++) { @SuppressWarnings("unchecked") DateEntry entry = ((DateEntry<K, V>)array[i]); output[i] = entry.value; } return output; } @Override public <T> T[] toArray(T[] a) { Object[] array; synchronized (_sync) { array = _map.values().toArray(); } for (int i=0; i < array.length; i++) { @SuppressWarnings("unchecked") DateEntry entry = ((DateEntry<K, V>)array[i]); @SuppressWarnings("unchecked") T value = (T)entry.value; a[i] = value; } return a; } @Override public boolean add(V v) { throw new UnsupportedOperationException(); } @Override public boolean remove(Object o) { throw new UnsupportedOperationException(); } @Override public boolean containsAll(Collection<?> c) { PreCon.notNull(c); @SuppressWarnings("unchecked") DateEntry<K, V> dateEntry = (DateEntry<K, V>)_entryPool.retrieve(); assert dateEntry != null; synchronized (_sync) { for (Object obj : c) { if (!_map.values().contains(dateEntry.asMatcher(obj))) return false; } } dateEntry.recycle(); return true; } @Override public boolean addAll(Collection<? extends V> c) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(Collection<?> c) { throw new UnsupportedOperationException(); } @Override public boolean retainAll(Collection<?> c) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } } private final class EntrySetWrapper implements Set<Entry<K, V>> { @Override public int size() { return TimedHashMap.this.size(); } @Override public boolean isEmpty() { return TimedHashMap.this.isEmpty(); } @Override public boolean contains(Object o) { return TimedHashMap.this.containsKey(o); } @Override public Iterator<Entry<K, V>> iterator() { return new ConversionIteratorWrapper<Entry<K, V>, Entry<K, DateEntry<K, V>>>(_strategy) { Iterator<Entry<K, DateEntry<K, V>>> iterator = _map.entrySet().iterator(); @Override protected Entry<K, V> convert(Entry<K, DateEntry<K, V>> internal) { return getEntry(internal); } @Override protected Iterator<Entry<K, DateEntry<K, V>>> iterator() { return iterator; } }; } @Override public Object[] toArray() { Object[] entries; synchronized (_sync) { entries =_map.entrySet().toArray(); } Object[] array = new Object[entries.length]; for (int i=0; i < entries.length; i++) { @SuppressWarnings("unchecked") Entry<K, DateEntry<K, V>> entry = (Entry<K, DateEntry<K, V>>)entries[i]; array[i] = getEntry(entry); } return array; } @Override public <T> T[] toArray(T[] a) { Object[] entries; synchronized (_sync) { entries =_map.entrySet().toArray(); } for (int i=0; i < entries.length; i++) { @SuppressWarnings("unchecked") Entry<K, DateEntry<K, V>> entry = (Entry<K, DateEntry<K, V>>)entries[i]; @SuppressWarnings("unchecked") T result = (T)getEntry(entry); a[i] = result; } return a; } @Override public boolean add(Entry<K, V> kvEntry) { throw new UnsupportedOperationException(); } @Override public boolean remove(Object o) { throw new UnsupportedOperationException(); } @Override public boolean containsAll(Collection<?> c) { synchronized (_sync) { for (Object obj : c) { if (obj instanceof Entry) { Entry entry = (Entry) obj; //noinspection SuspiciousMethodCalls DateEntry<K, V> value = _map.get(entry.getKey()); if (value == null) return false; //noinspection EqualsBetweenInconvertibleTypes if (!value.equals(entry.getValue())) return false; if (value.isExpired()) { //noinspection SuspiciousMethodCalls _map.remove(entry.getKey()); onLifespanEnd(getEntry(value.key, value.value)); value.recycle(); return false; } } else { return false; } } } return true; } @Override public boolean addAll(Collection<? extends Entry<K, V>> c) { throw new UnsupportedOperationException(); } @Override public boolean retainAll(Collection<?> c) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(Collection<?> c) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } } }