Skip to content

Instantly share code, notes, and snippets.

@JohnnyJayJay
Last active July 9, 2019 17:47
Show Gist options
  • Save JohnnyJayJay/a84ef8202dee5e8e939159f5537d043b to your computer and use it in GitHub Desktop.
Save JohnnyJayJay/a84ef8202dee5e8e939159f5537d043b to your computer and use it in GitHub Desktop.
modified event waiter from jda-utilities for jda 4 alpha
import net.dv8tion.jda.api.events.GenericEvent;
import net.dv8tion.jda.api.hooks.EventListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* This is an adaptation of jagrosh's EventWaiter from jda utilities
* (https://github.com/JDA-Applications/JDA-Utilities) for JDA 4 Alpha.
* Some changes have been made, but most of this code remained unchanged.
*
* <p>The EventExpecter is capable of handling specialized forms of
* {@link net.dv8tion.jda.api.events.GenericEvent GenericEvent} that must meet criteria not normally specifiable
* without implementation of an {@link net.dv8tion.jda.api.hooks.EventListener EventListener}.
*
* <p>Creating an EventExpecter requires provision and/or creation of a
* {@link java.util.concurrent.ScheduledExecutorService Executor}, and thus a proper
* shutdown of said executor.
*
* <p>For instances of this class to work, you need to register them as EventListeners to JDA.
*
* <p>As a final note, if you intend to use the EventExpecter, it is highly recommended you <b>DO NOT</b>
* create multiple EventExpecters! Doing this will cause unnecessary increases in memory usage.
*
*
* @author Johnny_JayJay / John Grosh (jagrosh)
*/
public class EventExpecter implements EventListener {
private final Map<Class<?>, Set<ExpectedEvent>> expectedEvents;
private final ScheduledExecutorService threadpool;
/**
* Constructs an empty EventExpecter.
*/
public EventExpecter() {
this(Executors.newSingleThreadScheduledExecutor());
}
/**
* Constructs an EventExpecter using the provided {@link java.util.concurrent.ScheduledExecutorService Executor}
* as it's threadpool.
*
* <p>A developer might choose to use this constructor over the {@link EventExpecter#EventExpecter() default},
* for using a alternate form of threadpool, as opposed to a {@link java.util.concurrent.Executors#newSingleThreadExecutor() single thread executor}.
* <p>
* It's worth noting that this EventExpecter can serve as a delegate to invoke the threadpool's shutdown via
* a call to {@link EventExpecter#shutdown()}.
*
* @param threadpool The ScheduledExecutorService to use for this EventExpecter's threadpool.
* @throws java.lang.IllegalArgumentException If the threadpool provided is {@code null} or
* {@link java.util.concurrent.ScheduledExecutorService#isShutdown() is shutdown}
* @see EventExpecter#shutdown()
*/
public EventExpecter(ScheduledExecutorService threadpool) {
checkNotNull(threadpool, "ScheduledExecutorService");
check(!threadpool.isShutdown(), "Cannot construct EventWaiter with a closed ScheduledExecutorService!");
this.expectedEvents = new HashMap<>();
this.threadpool = threadpool;
}
/**
* Gets whether the EventExpecter's internal ScheduledExecutorService
* {@link java.util.concurrent.ScheduledExecutorService#isShutdown() is shutdown}.
*
* @return {@code true} if the ScheduledExecutorService is shutdown, {@code false} otherwise.
*/
public boolean isShutdown() {
return threadpool.isShutdown();
}
/**
* Returns an instance of the builder-like class {@link ExpectationBuilder} to set up an event expectation conveniently.
*
* @param eventType The {@link java.lang.Class} of the Event to wait for.
* @param <T> The type of Event to wait for.
* @return A new instance of {@link ExpectationBuilder}.
*/
public <T extends GenericEvent> ExpectationBuilder<T> expect(Class<T> eventType) {
return new ExpectationBuilder<>(eventType);
}
/**
* Waits (not blocking) an indefinite amount of time for an {@link org.bukkit.event.Event} that
* returns {@code true} when tested with the provided {@link java.util.function.Predicate Predicate}.
*
* <p>When this occurs, the provided {@link java.util.function.Consumer Consumer} will accept and
* execute using the same Event.
*
* @param <T> The type of Event to wait for.
* @param eventType The {@link java.lang.Class} of the Event to wait for. Never null.
* @param condition The Predicate to test when Events of the provided type are thrown. Never null.
* @param action The Consumer to perform an action when the condition Predicate returns {@code true}. Never null.
* @throws IllegalArgumentException One of two reasons:
* <ul>
* <li>1) Either the {@code eventType}, {@code condition}, or {@code action} was {@code null}.</li>
* <li>2) The internal threadpool is shut down, meaning that no more tasks can be submitted.</li>
* </ul>
*/
public <T extends GenericEvent> void expect(Class<T> eventType, Predicate<T> condition, Consumer<T> action) {
expect(eventType, condition, action, -1, null, null);
}
/**
* Waits (not blocking) a predetermined amount of time for an {@link org.bukkit.event.Event} that
* returns {@code true} when tested with the provided {@link java.util.function.Predicate Predicate}.
*
* <p>Once started, there are two possible outcomes:
* <ul>
* <li>The correct Event occurs within the time allotted, and the provided
* {@link java.util.function.Consumer Consumer} will accept and execute using the same Event.</li>
*
* <li>The time limit is elapsed and the provided {@link java.lang.Runnable} is executed.</li>
* </ul>
*
* @param <T> The type of Event to wait for.
* @param eventType The {@link java.lang.Class} of the Event to wait for. Never null.
* @param condition The Predicate to test when Events of the provided type are thrown. Never null.
* @param action The Consumer to perform an action when the condition Predicate returns {@code true}. Never null.
* @param timeout The maximum amount of time to wait for, or {@code -1} if there is no timeout.
* @param unit The {@link java.util.concurrent.TimeUnit TimeUnit} measurement of the timeout, or
* {@code null} if there is no timeout.
* @param timeoutAction The Runnable to run if the time runs out before a correct Event is thrown, or
* {@code null} if there is no action on timeout.
* @throws IllegalArgumentException One of two reasons:
* <ul>
* <li>1) Either the {@code eventType}, {@code condition}, or {@code action} was {@code null}.</li>
* <li>2) The internal threadpool is shut down, meaning that no more tasks can be submitted.</li>
* </ul>
*/
public <T extends GenericEvent> void expect(Class<T> eventType, Predicate<T> condition, Consumer<T> action,
long timeout, TimeUnit unit, Runnable timeoutAction) {
check(!isShutdown(), "Attempted to register an ExpectedEvent while the EventExpecter's threadpool was already shut down!");
checkNotNull(eventType, "The provided class type");
checkNotNull(condition, "The provided condition predicate");
checkNotNull(action, "The provided action consumer");
ExpectedEvent we = new ExpectedEvent<>(condition, action);
Set<ExpectedEvent> set = expectedEvents.computeIfAbsent(eventType, c -> new HashSet<>());
set.add(we);
if (timeout > 0 && unit != null) {
threadpool.schedule(() ->
{
if (set.remove(we) && timeoutAction != null)
timeoutAction.run();
}, timeout, unit);
}
}
@SuppressWarnings("unchecked")
public final void onEvent(GenericEvent event) {
Class c = event.getClass();
// Runs at least once for the fired Event, at most
// once for each superclass (excluding Object) because
// Class#getSuperclass() returns null when the superclass
// is primitive, void, or (in this case) Object.
while (c != null) {
if (expectedEvents.containsKey(c)) {
Set<ExpectedEvent> set = expectedEvents.get(c);
set.removeAll(set.stream().filter((expectedEvent) -> expectedEvent.attempt(event)).collect(Collectors.toSet()));
}
c = c.getSuperclass();
}
}
/**
* Closes this EventExpecter.
*/
public void shutdown() {
threadpool.shutdown();
}
private class ExpectedEvent<T extends GenericEvent> {
final Predicate<T> condition;
final Consumer<T> action;
ExpectedEvent(Predicate<T> condition, Consumer<T> action) {
this.condition = condition;
this.action = action;
}
boolean attempt(T event) {
if (condition.test(event)) {
action.accept(event);
return true;
}
return false;
}
}
/**
* Class that is mainly used for chaining convenience when calling {@link #expect(Class)}.
*
* @param <T> The type of the event to expect.
* @see #expect(Class, Predicate, Consumer, long, TimeUnit, Runnable)
*
*/
public class ExpectationBuilder<T extends GenericEvent> {
private final Class<T> eventType;
private Predicate<T> condition = (e) -> true;
private Consumer<T> action = (e) -> {
};
private long timeout = -1;
private TimeUnit timeoutUnit = null;
private Runnable timeoutAction = null;
private ExpectationBuilder(Class<T> eventType) {
this.eventType = eventType;
}
/**
* Sets the condition for this expectation. If the Predicate returns true for a corresponding event,
* the Consumer set in {@link #thenAccept(Consumer)} will be called and the Expectation will be completed.
*
* @param condition The Predicate to test when Events of the provided type are thrown.
* @return this.
*/
public ExpectationBuilder<T> takeIf(Predicate<T> condition) {
this.condition = condition;
return this;
}
/**
* Sets the Consumer that will be called if the right event happens.
*
* @param action The Consumer to perform an action when the condition Predicate returns {@code true}.
* @return this.
*/
public ExpectationBuilder<T> thenAccept(Consumer<T> action) {
this.action = action;
return this;
}
/**
* Sets a timeout for the Expectation, i.e. after the the specified amount of time, the Expecter
* will stop waiting for the event. By default, there is no timeout.
*
* @param amount The maximum amount of time to wait for, or {@code -1} if there is no timeout.
* @param unit The {@link java.util.concurrent.TimeUnit TimeUnit} measurement of the timeout, or
* {@code null} if there is no timeout.
* @return this.
*/
public ExpectationBuilder<T> timeoutAfter(long amount, TimeUnit unit) {
this.timeout = amount;
this.timeoutUnit = unit;
return this;
}
/**
* Sets the action to perform if the Expectation times out.
*
* @param action The Runnable to run if the time runs out before a correct Event is thrown, or
* {@code null} if there is no action on timeout.
* @return this.
*/
public ExpectationBuilder<T> runOnTimeout(Runnable action) {
this.timeoutAction = action;
return this;
}
/**
* Finishes the Expectation setup and tells the EventExpecter to wait for the event (not blocking).
* This method simply calls {@link #expect(Class, Predicate, Consumer, long, TimeUnit, Runnable)},
* so it behaves exactly the same way.
*/
public void register() {
EventExpecter.this.expect(eventType, condition, action, timeout, timeoutUnit, timeoutAction);
}
}
private static void check(boolean condition, String message) {
if (!condition)
throw new IllegalArgumentException(message);
}
private static void checkNotNull(Object argument, String name) {
check(argument != null, name + " may not be null");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment