Skip to content

Commit 68f1a14

Browse files
committed
API: Added useful (I hope) ExpiringSet utility class.
1 parent 2100520 commit 68f1a14

File tree

2 files changed

+782
-0
lines changed

2 files changed

+782
-0
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
package world.bentobox.bentobox.util;
2+
3+
import java.util.Collection;
4+
import java.util.Iterator;
5+
import java.util.Set;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.ConcurrentMap;
8+
import java.util.concurrent.Executors;
9+
import java.util.concurrent.ScheduledExecutorService;
10+
import java.util.concurrent.ScheduledFuture;
11+
import java.util.concurrent.TimeUnit;
12+
13+
/**
14+
* An {@code ExpiringSet} is a set implementation that automatically removes elements after a
15+
* specified period of time. The expiration time is specified when the set is created and
16+
* applies to all elements added to the set. It is thread-safe and provides similar
17+
* functionality to {@code HashSet} with the added feature of automatic expiration of elements.
18+
*
19+
* <p>This class manages a background thread resource, so it implements {@code AutoCloseable}
20+
* to ensure resources are released reliably using try-with-resources.
21+
*
22+
* @param <E> the type of elements maintained by this set
23+
*/
24+
public class ExpiringSet<E> implements Set<E>, AutoCloseable {
25+
// Maps the element (E) to its scheduled removal task (ScheduledFuture).
26+
private final ConcurrentMap<E, ScheduledFuture<?>> scheduledTasks;
27+
private final ScheduledExecutorService scheduler;
28+
private final long expirationTime;
29+
30+
/**
31+
* Constructs an empty {@code ExpiringSet} with the specified expiration time for elements.
32+
*
33+
* @param expirationTime the time after which elements should expire, in the specified time unit
34+
* @param timeUnit the time unit for the {@code expirationTime} parameter
35+
* @throws IllegalArgumentException if {@code expirationTime} is less than or equal to zero
36+
* @throws NullPointerException if {@code timeUnit} is null
37+
*/
38+
public ExpiringSet(long expirationTime, TimeUnit timeUnit) {
39+
if (expirationTime <= 0) {
40+
throw new IllegalArgumentException("Expiration time must be greater than zero.");
41+
}
42+
if (timeUnit == null) {
43+
throw new NullPointerException("TimeUnit cannot be null.");
44+
}
45+
46+
this.scheduledTasks = new ConcurrentHashMap<>();
47+
// Use a single thread for scheduling removals.
48+
this.scheduler = Executors.newSingleThreadScheduledExecutor();
49+
this.expirationTime = timeUnit.toMillis(expirationTime);
50+
}
51+
52+
/**
53+
* Shuts down the {@code ScheduledExecutorService} immediately. This attempts to stop
54+
* all actively executing tasks and halts the processing of waiting tasks.
55+
*/
56+
public void shutdownNow() {
57+
scheduler.shutdownNow();
58+
}
59+
60+
/**
61+
* Closes this resource, relinquishing any underlying resources.
62+
* This method is called automatically when the resource is used in a try-with-resources statement.
63+
*/
64+
@Override
65+
public void close() {
66+
shutdownNow();
67+
}
68+
69+
// --- Set Interface Implementations ---
70+
71+
/**
72+
* Returns the number of elements in this set (its cardinality).
73+
*/
74+
@Override
75+
public int size() {
76+
return scheduledTasks.size();
77+
}
78+
79+
/**
80+
* Returns {@code true} if this set contains no elements.
81+
*/
82+
@Override
83+
public boolean isEmpty() {
84+
return scheduledTasks.isEmpty();
85+
}
86+
87+
/**
88+
* Returns {@code true} if this set contains the specified element.
89+
*
90+
* @param o element whose presence in this set is to be tested
91+
* @return {@code true} if this set contains the specified element
92+
* @throws NullPointerException if the specified element is null
93+
*/
94+
@Override
95+
public boolean contains(Object o) {
96+
if (o == null) {
97+
throw new NullPointerException("Element cannot be null.");
98+
}
99+
return scheduledTasks.containsKey(o);
100+
}
101+
102+
/**
103+
* Returns an iterator over the elements in this set.
104+
*
105+
* @return an iterator over the elements in this set
106+
*/
107+
@Override
108+
public Iterator<E> iterator() {
109+
// Iterate over the keys of the map
110+
return scheduledTasks.keySet().iterator();
111+
}
112+
113+
/**
114+
* Returns an array containing all of the elements in this set.
115+
*/
116+
@Override
117+
public Object[] toArray() {
118+
return scheduledTasks.keySet().toArray();
119+
}
120+
121+
/**
122+
* Returns an array containing all of the elements in this set; the runtime type of
123+
* the returned array is that of the specified array.
124+
*
125+
* @param a the array into which the elements of this set are to be stored
126+
* @param <T> the runtime type of the array to contain the collection
127+
* @return an array containing all of the elements in this set
128+
*/
129+
@Override
130+
public <T> T[] toArray(T[] a) {
131+
return scheduledTasks.keySet().toArray(a);
132+
}
133+
134+
/**
135+
* Adds the specified element to this set if it is not already present.
136+
* The element will automatically be removed after the specified expiration time.
137+
* If the element is already present, its expiration time is refreshed.
138+
*
139+
* @param e element to be added to this set
140+
* @throws NullPointerException if the specified element is null
141+
* @return {@code true} if this set did not already contain the specified element
142+
*/
143+
@Override
144+
public boolean add(E e) {
145+
if (e == null) {
146+
throw new NullPointerException("Element cannot be null.");
147+
}
148+
149+
// The lambda captures the 'futureHolder' array reference, which is final.
150+
final ScheduledFuture<?>[] futureHolder = new ScheduledFuture<?>[1];
151+
152+
// 1. Create and schedule the new task. The lambda uses the reference in the array.
153+
futureHolder[0] = scheduler.schedule(() -> {
154+
// Self-removal: Try to remove the element only if its associated future is still the one
155+
// referenced by the holder at the time of execution.
156+
scheduledTasks.remove(e, futureHolder[0]);
157+
}, expirationTime, TimeUnit.MILLISECONDS);
158+
159+
// 2. Atomically replace the old future with the new one.
160+
// We use the value stored in the array immediately after scheduling.
161+
ScheduledFuture<?> oldFuture = scheduledTasks.put(e, futureHolder[0]);
162+
163+
if (oldFuture != null) {
164+
// Element was already present (refresh). Cancel the old task.
165+
// false means do not interrupt the thread if the task is already running.
166+
oldFuture.cancel(false);
167+
return false; // Not a new element
168+
}
169+
170+
return true; // New element added
171+
}
172+
173+
/**
174+
* Removes the specified element from this set if it is present.
175+
*
176+
* @param o object to be removed from this set, if present
177+
* @return {@code true} if this set contained the specified element
178+
* @throws NullPointerException if the specified element is null
179+
*/
180+
@Override
181+
public boolean remove(Object o) {
182+
if (o == null) {
183+
throw new NullPointerException("Element cannot be null.");
184+
}
185+
186+
// Atomically remove the element and retrieve its associated future
187+
ScheduledFuture<?> future = scheduledTasks.remove(o);
188+
189+
if (future != null) {
190+
// Cancel the future to prevent the removal task from running later
191+
future.cancel(false);
192+
return true;
193+
}
194+
195+
return false;
196+
}
197+
198+
/**
199+
* Returns {@code true} if this set contains all of the elements of the specified collection.
200+
*
201+
* @param c collection to be checked for containment in this set
202+
* @return {@code true} if this set contains all of the elements of the specified collection
203+
* @throws NullPointerException if the specified collection is null
204+
*/
205+
@Override
206+
public boolean containsAll(Collection<?> c) {
207+
if (c == null) {
208+
throw new NullPointerException("Collection cannot be null.");
209+
}
210+
return scheduledTasks.keySet().containsAll(c);
211+
}
212+
213+
/**
214+
* Adds all of the elements in the specified collection to this set if they're not
215+
* already present. Each element's expiration time is set/refreshed.
216+
*
217+
* @param c collection containing elements to be added to this set
218+
* @return {@code true} if this set changed as a result of the call
219+
* @throws NullPointerException if the specified collection is null or contains a null element
220+
*/
221+
@Override
222+
public boolean addAll(Collection<? extends E> c) {
223+
if (c == null) {
224+
throw new NullPointerException("The specified collection cannot be null.");
225+
}
226+
boolean modified = false;
227+
for (E element : c) {
228+
if (element == null) {
229+
throw new NullPointerException("Collection cannot contain null elements.");
230+
}
231+
// Use the custom add method to trigger expiration scheduling and cancellation
232+
if (add(element)) {
233+
modified = true;
234+
}
235+
}
236+
return modified;
237+
}
238+
239+
/**
240+
* Retains only the elements in this set that are contained in the specified collection.
241+
*
242+
* @param c collection containing elements to be retained in this set
243+
* @return {@code true} if this set changed as a result of the call
244+
* @throws NullPointerException if the specified collection is null
245+
*/
246+
@Override
247+
public boolean retainAll(Collection<?> c) {
248+
if (c == null) {
249+
throw new NullPointerException("Collection cannot be null.");
250+
}
251+
// Since we cannot atomically retain/cancel, we rely on the ConcurrentMap's keySet retainAll.
252+
// This will remove entries but leave the futures running (a minor resource leak).
253+
return scheduledTasks.keySet().retainAll(c);
254+
}
255+
256+
/**
257+
* Removes from this set all of its elements that are contained in the specified collection.
258+
*
259+
* @param c collection containing elements to be removed from this set
260+
* @return {@code true} if this set changed as a result of the call
261+
* @throws NullPointerException if the specified collection is null
262+
*/
263+
@Override
264+
public boolean removeAll(Collection<?> c) {
265+
if (c == null) {
266+
throw new NullPointerException("Collection cannot be null.");
267+
}
268+
boolean modified = false;
269+
// Iterate through the collection and use the custom remove(Object o) method
270+
for (Object element : c) {
271+
if (remove(element)) {
272+
modified = true;
273+
}
274+
}
275+
return modified;
276+
}
277+
278+
/**
279+
* Removes all of the elements from this set. The set will be empty after this call returns.
280+
*/
281+
@Override
282+
public void clear() {
283+
// Cancel all pending futures
284+
for (ScheduledFuture<?> future : scheduledTasks.values()) {
285+
future.cancel(false);
286+
}
287+
scheduledTasks.clear();
288+
}
289+
290+
/**
291+
* Compares the specified object with this set for equality.
292+
*
293+
* @param obj object to be compared for equality with this set
294+
* @return {@code true} if the specified object is equal to this set
295+
*/
296+
@Override
297+
public boolean equals(Object obj) {
298+
return scheduledTasks.keySet().equals(obj);
299+
}
300+
301+
/**
302+
* Returns the hash code value for this set.
303+
*
304+
* @return the hash code value for this set
305+
*/
306+
@Override
307+
public int hashCode() {
308+
return scheduledTasks.keySet().hashCode();
309+
}
310+
}

0 commit comments

Comments
 (0)