/* * 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.ConversionListIteratorWrapper; import com.jcwhatever.nucleus.collections.wrap.ConversionListWrapper; import com.jcwhatever.nucleus.collections.wrap.IteratorWrapper; import com.jcwhatever.nucleus.collections.wrap.SyncStrategy; import com.jcwhatever.nucleus.managed.scheduler.IScheduledTask; import com.jcwhatever.nucleus.managed.scheduler.Scheduler; import com.jcwhatever.nucleus.mixins.IPluginOwned; import com.jcwhatever.nucleus.utils.CollectionUtils; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.Rand; 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.utils.performance.pool.IPoolElementFactory; import com.jcwhatever.nucleus.utils.performance.pool.SimpleConcurrentPool; import com.jcwhatever.nucleus.utils.validate.IValidator; import org.bukkit.plugin.Plugin; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.WeakHashMap; import javax.annotation.Nullable; /** * An array list where each item has an individual lifespan that when reached, causes * the item to be removed. * * <p>The items lifespan cannot be reset except by removing it.</p> * * <p>Items can be added using the default lifespan or a lifespan can be specified * per item.</p> * * <p>Subscribers that are added to track when an item expires or when 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>Note that the indexing operations of the list should be used with care or avoided. * It is possible to check the size of the list then use an indexing operation and have * a difference between the size when checked and the size when the indexing operation * is performed. The list will not remove an expired item if retrieved through an * indexing operation, however the janitor (removes expired items at interval) may remove * an element between operations from a different thread.</p> * * <p>Thread safe.</p> * * <p>The lists iterators must be used inside a synchronized block which locks the * list instance. Otherwise, a {@link java.lang.IllegalStateException} is thrown.</p> */ public class TimedArrayList<E> implements List<E>, 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, 9); private final static Map<TimedArrayList, Void> _instances = new WeakHashMap<>(10); private static IScheduledTask _janitor; private final Plugin _plugin; private final List<Element<E>> _list; private final int _lifespan; // milliseconds private final TimeScale _timeScale; private final transient Object _sync; private transient long _nextCleanup; private final transient NamedUpdateAgents _agents = new NamedUpdateAgents(); private final transient SimpleConcurrentPool<Element> _elementPool; /** * Constructor. * * <p>Default item lifespan is 20 ticks.</p> * * <p>Initial capacity is 10.</p> */ public TimedArrayList(Plugin plugin) { this(plugin, 10, 20, TimeScale.TICKS); } /** * Constructor. * * <p>Default item lifespan is 20 ticks.</p> * * @param capacity The initial capacity of the list. */ public TimedArrayList(Plugin plugin, int capacity) { this(plugin, capacity, 20, TimeScale.TICKS); } /** * Constructor. * * @param capacity The initial capacity of the list. * @param defaultTime The default lifespan of items. * @param timeScale The lifespan time scale. */ public TimedArrayList(Plugin plugin, int capacity, int defaultTime, TimeScale timeScale) { PreCon.notNull(plugin); PreCon.positiveNumber(defaultTime); PreCon.notNull(timeScale); _plugin = plugin; _lifespan = defaultTime * timeScale.getTimeFactor(); _timeScale = timeScale; _list = new ArrayList<>(capacity); _sync = this; _elementPool = new SimpleConcurrentPool<Element>(50, new IPoolElementFactory<Element>() { @Override public Element create() { return new Element<>(TimedArrayList.this); } }); synchronized (_instances) { _instances.put(this, null); } startJanitor(); } /** * Add an item to the list and specify its lifetime using * the time scale specified in the constructor. * * @param item The item to add. * @param lifespan The amount of time the element will stay in the list. */ public boolean add(final E item, int lifespan) { return add(item, lifespan, _timeScale); } /** * Add an item to the list and specify its lifetime using the * specified time scale. * * @param item The item to add. * @param lifespan The amount of time the element will stay in the list. * @param timeScale The time scale of the specified lifespan. */ public boolean add(final E item, int lifespan, TimeScale timeScale) { PreCon.notNull(item); PreCon.positiveNumber(lifespan); PreCon.notNull(timeScale); @SuppressWarnings("unchecked") Element<E> nelm = (Element<E>)_elementPool.retrieve(); assert nelm != null; synchronized (_sync) { return _list.add(nelm.asElement(item, lifespan, timeScale)); } } /** * Insert an item into the list at the specified index * and specify its lifespan using the time scale specified * in the constructor. * * @param index The index position to insert at. * @param item The item to insert. * @param lifespan The amount of time in the element will stay in the list. */ public void add(int index, E item, int lifespan) { add(index, item, lifespan, _timeScale); } /** * Insert an item into the list at the specified index * and specify its lifespan using the the time scale specified. * * @param index The index position to insert at. * @param item The item to insert. * @param lifespan The amount of time in the element will stay in the list. * @param timeScale The time scale of the specified lifespan. */ public void add(int index, E item, int lifespan, TimeScale timeScale) { PreCon.positiveNumber(index); PreCon.notNull(item); PreCon.positiveNumber(lifespan); PreCon.notNull(timeScale); @SuppressWarnings("unchecked") Element<E> nelm = (Element<E>)_elementPool.retrieve(); assert nelm != null; synchronized (_sync) { _list.add(index, nelm.asElement(item, lifespan, timeScale)); } } /** * Add a collection to the list and specify the lifespan using the * time scale specified in the constructor. * * @param collection The collection to add. * @param lifespan The amount of time in ticks it will stay in the list. */ public boolean addAll(Collection<? extends E> collection, int lifespan) { return addAll(collection, lifespan, _timeScale); } /** * Add a collection to the list and specify the lifespan using the * specified time scale. * * @param collection The collection to add. * @param lifespan The amount of time in the element will stay in the list. * @param timeScale The time scale of the specified lifespan. */ public boolean addAll(Collection<? extends E> collection, int lifespan, TimeScale timeScale) { PreCon.notNull(collection); PreCon.positiveNumber(lifespan); PreCon.notNull(timeScale); boolean isChanged = false; for (E item : collection) { isChanged = add(item, lifespan, timeScale) || isChanged; } return isChanged; } /** * Insert a collection into the list at the specified index * and specify the lifespan using the time scale specified * in the constructor. * * @param index The index position to insert at. * @param collection The collection to add. * @param lifespan The amount of time in the element will stay in the list. */ public boolean addAll(int index, Collection<? extends E> collection, int lifespan) { return addAll(index, collection, lifespan, _timeScale); } /** * Insert a collection into the list at the specified index * and specify the lifespan using the specified time scale. * * @param index The index position to insert at. * @param collection The collection to add. * @param lifespan The amount of time in ticks it will stay in the list. * @param timeScale The time scale of the specified lifespan. */ public boolean addAll(int index, Collection<? extends E> collection, int lifespan, TimeScale timeScale) { PreCon.positiveNumber(index); PreCon.notNull(collection); PreCon.positiveNumber(lifespan); PreCon.notNull(timeScale); List<Element<E>> list = new ArrayList<>(collection.size()); for (E item : collection) { @SuppressWarnings("unchecked") Element<E> nelm = (Element<E>)_elementPool.retrieve(); assert nelm != null; list.add(nelm.asElement(item, lifespan, timeScale)); } synchronized (_sync) { return _list.addAll(index, list); } } /** * 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 TimedArrayList<E> setMaxPoolSize(int poolSize) { _elementPool.setMaxSize(poolSize); return this; } /** * Get the maximum size of the internal object pool used * for pooling internal instances. */ public int getMaxPoolSize() { return _elementPool.maxSize(); } /** * Subscribe to updates called when an elements lifespan ends. * * @param subscriber The lifespan end subscriber. * * @return Self for chaining. */ public TimedArrayList<E> onLifespanEnd(IUpdateSubscriber<E> subscriber) { PreCon.notNull(subscriber); _agents.getAgent("onLifespanEnd").addSubscriber(subscriber); return this; } /** * Subscribe to updates called when the collection is empty. * * @param subscriber The subscriber. * * @return Self for chaining. */ public TimedArrayList<E> onEmpty(IUpdateSubscriber<TimedArrayList<E>> subscriber) { PreCon.notNull(subscriber); _agents.getAgent("onEmpty").addSubscriber(subscriber); return this; } @Override public Plugin getPlugin() { return _plugin; } @Override public int size() { synchronized (_sync) { cleanup(); return _list.size(); } } @Override public boolean isEmpty() { synchronized (_sync) { cleanup(); return _list.isEmpty(); } } @Override public boolean contains(Object o) { synchronized (_sync) { Iterator<Element<E>> iterator = _list.iterator(); while (iterator.hasNext()) { Element<E> entry = iterator.next(); if (entry.isExpired()) { iterator.remove(); onLifespanEnd(entry.element); _elementPool.recycle(entry); continue; } if (o == null && entry.element == null) return true; if (o != null && o.equals(entry.element)) return true; } return false; } } @Override public Iterator<E> iterator() { return new Itr(); } @Override public Object[] toArray() { synchronized (_sync) { cleanup(); Object[] array = new Object[_list.size()]; for (int i = 0; i < array.length; i++) { array[i] = _list.get(i).element; } return array; } } @Override public <T> T[] toArray(T[] array) { synchronized (_sync) { cleanup(); for (int i = 0; i < array.length; i++) { @SuppressWarnings("unchecked") T item = (T) _list.get(i).element; array[i] = item; } return array; } } @Override public boolean add(E item) { return add(item, _lifespan, TimeScale.MILLISECONDS); } @Override public void add(int index, E item) { PreCon.positiveNumber(index); PreCon.notNull(item); add(index, item, _lifespan, TimeScale.MILLISECONDS); } @Override public boolean addAll(Collection<? extends E> collection) { PreCon.notNull(collection); return addAll(collection, _lifespan, TimeScale.MILLISECONDS); } @Override public boolean addAll(int index, Collection<? extends E> collection) { PreCon.positiveNumber(index); PreCon.notNull(collection); return addAll(index, collection, _lifespan, TimeScale.MILLISECONDS); } @Override public void clear() { synchronized (_sync) { for (Element<E> element : _list) { element.recycle(); } _list.clear(); } } @Override public E get(int index) { synchronized (_sync) { Element<E> entry = _list.get(index); return entry.element; } } @Override @Nullable public E set(int index, E element) { synchronized (_sync) { @SuppressWarnings("unchecked") Element<E> nelm = (Element<E>)_elementPool.retrieve(); assert nelm != null; Element<E> previous = _list.set(index, nelm.asElement(element, _lifespan, TimeScale.MILLISECONDS)); if (previous == null) return null; E prev = previous.element; previous.recycle(); return prev; } } @Override public boolean remove(Object item) { PreCon.notNull(item); synchronized (_sync) { @SuppressWarnings("unchecked") Element<E> nelm = (Element<E>)_elementPool.retrieve(); assert nelm != null; //noinspection unchecked return _list.remove(nelm.asMatcher(item)); } } @Override public boolean containsAll(Collection<?> c) { PreCon.notNull(c); synchronized (_sync) { cleanup(); @SuppressWarnings("unchecked") Element<E> nelm = (Element<E>)_elementPool.retrieve(); assert nelm != null; for (Object obj : c) { if (!_list.contains(nelm.asMatcher(obj))) return false; } _elementPool.recycle(nelm); return true; } } @Override @Nullable public E remove(int index) { PreCon.positiveNumber(index); synchronized (_sync) { Element<E> previous = _list.remove(index); if (previous == null) return null; E prev = previous.element; previous.recycle(); return prev; } } @Override public int indexOf(Object o) { PreCon.notNull(o); synchronized (_sync) { int i = 0; Iterator<Element<E>> iterator = _list.iterator(); while (iterator.hasNext()) { Element<E> entry = iterator.next(); if (entry.isExpired()) { iterator.remove(); onLifespanEnd(entry.element); entry.recycle(); i--; } else if (o.equals(entry.element)) { return i; } i++; } return -1; } } @Override public int lastIndexOf(Object o) { PreCon.notNull(o); synchronized (_sync) { for (int i = _list.size() - 1; i >= 0; i--) { Element<E> entry = _list.get(i); if (entry.isExpired()) continue; if (entry.element.equals(o)) return i; } return -1; } } @Override public ListIterator<E> listIterator() { return new TimedListIterator(0); } @Override public ListIterator<E> listIterator(int index) { return new TimedListIterator(index); } @Override public List<E> subList(int fromIndex, int toIndex) { synchronized (_sync) { final List<Element<E>> subList = _list.subList(fromIndex, toIndex); return new ConversionListWrapper<E, Element<E>>() { @Override protected List<Element<E>> list() { return subList; } @Override protected E convert(Element<E> internal) { return internal.element; } @Override protected Element<E> unconvert(Object external) { @SuppressWarnings("unchecked") E element = (E)external; @SuppressWarnings("unchecked") Element<E> nelm = (Element<E>)_elementPool.retrieve(); assert nelm != null; return nelm.asElement(element, _lifespan, _timeScale); } }; } } @Override public boolean removeAll(Collection<?> collection) { PreCon.notNull(collection); boolean isChanged = false; synchronized (_sync) { for (Object item : collection) { isChanged = remove(item) || isChanged; } } return isChanged; } @Override public boolean retainAll(final Collection<?> c) { PreCon.notNull(c); return !CollectionUtils.retainAll(_list, new IValidator<Element<E>>() { @Override public boolean isValid(Element<E> element) { return c.contains(element.element); } }).isEmpty(); } private void onLifespanEnd(E item) { _agents.update("onLifespanEnd", item); if (isEmpty()) { _agents.update("onEmpty", this); } } private void cleanup() { if (_list.isEmpty()) return; // prevent cleanup from running too often if (_nextCleanup > System.currentTimeMillis()) return; _nextCleanup = System.currentTimeMillis() + MIN_CLEANUP_INTERVAL_MS; int size = _list.size(); // remove backwards to reduce the amount of // element shifting for (int i = size - 1; i >= 0; i--) { Element<E> element = _list.get(i); if (element.isExpired()) { _list.remove(i); onLifespanEnd(element.element); element.recycle(); } } } private void startJanitor() { if (_janitor != null) return; _janitor = Scheduler.runTaskRepeatAsync(Nucleus.getPlugin(), JANITOR_INITIAL_DELAY_TICKS, JANITOR_INTERVAL_TICKS, new Runnable() { List<TimedArrayList> lists = new ArrayList<TimedArrayList>(20); @Override public void run() { synchronized (_instances) { lists.addAll(_instances.keySet()); } for (TimedArrayList list : lists) { if (!list.getPlugin().isEnabled()) { synchronized (_instances) { _instances.remove(list); } continue; } synchronized (list._sync) { list.cleanup(); } } lists.clear(); } }); } private final static class Element<T> { TimedArrayList<T> parent; T element; long expires; Object matcher; boolean isRecycled; Element(TimedArrayList<T> parent) { this.parent = parent; } Element<T> asElement(T item, long lifespan, TimeScale timeScale) { this.element = item; this.expires = System.currentTimeMillis() + (lifespan * timeScale.getTimeFactor()); this.matcher = item; this.isRecycled = false; return this; } Element<T> asMatcher(Object matcher) { this.element = null; this.expires = 0; this.matcher = matcher; this.isRecycled = false; return this; } boolean isExpired() { return System.currentTimeMillis() >= expires; } void recycle() { if (this.isRecycled) return; this.isRecycled = true; this.element = null; this.matcher = null; parent._elementPool.recycle(this); } @Override public int hashCode() { return element != null ? element.hashCode() : 0; } @Override public boolean equals(Object obj) { if (obj instanceof Element) { obj = ((Element) obj).matcher; } if (matcher == null && obj == null) return true; if (matcher != null && matcher.equals(obj)) return true; return false; } } private final class TimedListIterator extends ConversionListIteratorWrapper<E, Element<E>> { ListIterator<Element<E>> iterator; TimedListIterator(int index) { super(new SyncStrategy(TimedArrayList.this._sync)); this.iterator = _list.listIterator(index); } @Override protected E convert(Element<E> internal) { return internal.element; } @Override protected Element<E> unconvert(Object external) { @SuppressWarnings("unchecked") E e = (E)external; @SuppressWarnings("unchecked") Element<E> nelm = (Element<E>)_elementPool.retrieve(); assert nelm != null; return nelm.asElement(e, _lifespan, _timeScale); } @Override protected ListIterator<Element<E>> iterator() { return iterator; } } private final class Itr implements Iterator<E> { final Iterator<Element<E>> iterator = _list.iterator(); Element<E> peek; boolean invokedHasNext; boolean invokedNext; @Override public boolean hasNext() { IteratorWrapper.assertIteratorLock(_sync); invokedHasNext = true; if (!iterator.hasNext()) return false; while (iterator.hasNext()) { peek = iterator.next(); if (peek.isExpired()) { iterator.remove(); onLifespanEnd(peek.element); peek.recycle(); } else { return true; } } return false; } @Override public E next() { IteratorWrapper.assertIteratorLock(_sync); if (!invokedHasNext) throw new IllegalStateException("Cannot invoke 'next' until 'hasNext' has been invoked."); invokedHasNext = false; invokedNext = true; if (peek == null) hasNext(); if (peek == null) throw new NoSuchElementException(); Element<E> n = peek; peek = null; return n.element; } @Override public void remove() { IteratorWrapper.assertIteratorLock(_sync); if (!invokedNext) throw new IllegalStateException("Cannot 'remove' until 'next' method is invoked."); invokedNext = false; iterator.remove(); } } }