-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
419 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
src/main/java/de/bluecolored/bluemap/api/events/APIEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package de.bluecolored.bluemap.api.events; | ||
|
||
import de.bluecolored.bluemap.api.BlueMapAPI; | ||
|
||
/** | ||
* All events involving any change with {@link BlueMapAPI} | ||
*/ | ||
public abstract class APIEvent { | ||
|
||
private final BlueMapAPI api; | ||
|
||
public APIEvent(BlueMapAPI api) { | ||
this.api = api; | ||
} | ||
|
||
/** | ||
* Returns the {@link BlueMapAPI} instance involved with this event. | ||
* @return The {@link BlueMapAPI} instance | ||
*/ | ||
public BlueMapAPI getAPI() { | ||
return api; | ||
} | ||
|
||
/** | ||
* Called when {@link BlueMapAPI} got enabled and is now available. | ||
*/ | ||
public static class Enable extends APIEvent { | ||
|
||
|
||
|
||
public Enable(BlueMapAPI api) { | ||
super(api); | ||
} | ||
|
||
} | ||
|
||
/** | ||
* Called when {@link BlueMapAPI} gets disabled and will soon be no longer available. | ||
*/ | ||
public static class Disable extends APIEvent { | ||
|
||
public Disable(BlueMapAPI api) { | ||
super(api); | ||
} | ||
|
||
} | ||
|
||
} |
17 changes: 17 additions & 0 deletions
17
src/main/java/de/bluecolored/bluemap/api/events/EventConsumer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package de.bluecolored.bluemap.api.events; | ||
|
||
/** | ||
* A consumer accepting events | ||
* @param <T> The type of events that this consumer accepts | ||
*/ | ||
@FunctionalInterface | ||
public interface EventConsumer<T> { | ||
|
||
/** | ||
* Performs an action on the provided event. | ||
* @param event The event | ||
* @throws Exception If an exception occurred while handling the event | ||
*/ | ||
void accept(T event) throws Exception; | ||
|
||
} |
82 changes: 82 additions & 0 deletions
82
src/main/java/de/bluecolored/bluemap/api/events/EventDispatcher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package de.bluecolored.bluemap.api.events; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.List; | ||
import java.util.concurrent.locks.ReadWriteLock; | ||
import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
|
||
/** | ||
* A dispatcher able to dispatch events of a specific type to registered {@link EventConsumer}s. | ||
* @param <T> The type of events this dispatcher dispatches | ||
*/ | ||
@SuppressWarnings("unused") | ||
public class EventDispatcher<T> { | ||
|
||
private final Collection<EventConsumer<? super T>> listeners = new ArrayList<>(); | ||
|
||
private final ReadWriteLock lock = new ReentrantReadWriteLock(); | ||
|
||
/** | ||
* Instances of this class should only be acquired using {@link Events#getDispatcher(Class)} | ||
*/ | ||
EventDispatcher() {} | ||
|
||
/** | ||
* Dispatches the given event to all {@link EventConsumer}s registered to this dispatcher. | ||
* @param event The event that should be dispatched | ||
* @throws Exception If one or more of the {@link EventConsumer}s threw an exception | ||
*/ | ||
public void dispatch(T event) throws Exception { | ||
List<Exception> thrownExceptions = null; | ||
|
||
lock.readLock().lock(); | ||
try { | ||
for (EventConsumer<? super T> listener : listeners) { | ||
try { | ||
listener.accept(event); | ||
} catch (Exception e) { | ||
if (thrownExceptions == null) thrownExceptions = new ArrayList<>(1); | ||
thrownExceptions.add(e); | ||
} | ||
} | ||
} finally { | ||
lock.readLock().unlock(); | ||
} | ||
|
||
if (thrownExceptions != null && !thrownExceptions.isEmpty()) { | ||
Exception ex = thrownExceptions.get(0); | ||
for (int i = 1; i < thrownExceptions.size(); i++) { | ||
ex.addSuppressed(thrownExceptions.get(i)); | ||
} | ||
throw ex; | ||
} | ||
} | ||
|
||
/** | ||
* Adds an {@link EventConsumer} to this dispatcher. | ||
* @param listener The {@link EventConsumer} to be added | ||
*/ | ||
public void addListener(EventConsumer<? super T> listener) { | ||
lock.writeLock().lock(); | ||
try { | ||
listeners.add(listener); | ||
} finally { | ||
lock.writeLock().unlock(); | ||
} | ||
} | ||
|
||
/** | ||
* Removes an {@link EventConsumer} from this dispatcher. | ||
* @param listener The {@link EventConsumer} to be removed | ||
*/ | ||
public boolean removeListener(EventConsumer<?> listener) { | ||
lock.writeLock().lock(); | ||
try { | ||
return listeners.remove(listener); | ||
} finally { | ||
lock.writeLock().unlock(); | ||
} | ||
} | ||
|
||
} |
175 changes: 175 additions & 0 deletions
175
src/main/java/de/bluecolored/bluemap/api/events/Events.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
package de.bluecolored.bluemap.api.events; | ||
|
||
import org.jetbrains.annotations.Nullable; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
import java.lang.ref.WeakReference; | ||
import java.lang.reflect.Method; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
/** | ||
* Utility class to register and dispatch BlueMapAPI-Events. | ||
*/ | ||
@SuppressWarnings({"unused", "UnusedReturnValue"}) | ||
public final class Events { | ||
|
||
private static final Collection<ListenerRegistration<?>> registrations = new ArrayList<>(); | ||
private static final Map<Class<?>, EventDispatcher<?>> dispatchers = new HashMap<>(); | ||
|
||
private Events() { | ||
throw new UnsupportedOperationException("Utility class"); | ||
} | ||
|
||
/** | ||
* Returns the event-dispatcher for a specific event-class. | ||
* @param eventClass The event-class of the dispatcher | ||
* @return The event-dispatcher | ||
* @param <T> The type of the event | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public static synchronized <T> EventDispatcher<T> getDispatcher(Class<T> eventClass) { | ||
return (EventDispatcher<T>) dispatchers.computeIfAbsent(eventClass, c -> { | ||
EventDispatcher<T> dispatcher = new EventDispatcher<>(); | ||
|
||
for (ListenerRegistration<?> reg : registrations) { | ||
if (!reg.getEventClass().isAssignableFrom(eventClass)) continue; | ||
ListenerRegistration<? super T> registration = (ListenerRegistration<? super T>) reg; | ||
|
||
EventConsumer<? super T> listener = registration.getListener(); | ||
if (listener != null) dispatcher.addListener(listener); | ||
} | ||
|
||
return dispatcher; | ||
}); | ||
} | ||
|
||
/** | ||
* Registers an {@link EventConsumer} that will be invoked for every event of the eventClass-type AND any subclass. | ||
* @param eventClass The (super-)class of the event that should be listened for | ||
* @param addon The addon/plugin/mod instance (can be used in {@link #unregisterListeners(Object)} | ||
* to unregister all listeners of this addon) | ||
* @param listener The {@link EventConsumer} that will be invoked | ||
* @param <T> The event-type | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public static synchronized <T> void registerListener(Class<T> eventClass, Object addon, EventConsumer<? super T> listener) { | ||
ListenerRegistration<T> registration = new ListenerRegistration<>(eventClass, addon, listener); | ||
|
||
registrations.add(registration); | ||
|
||
dispatchers.forEach((dispatcherEventClass, dispatcher) -> { | ||
if (eventClass.isAssignableFrom(dispatcherEventClass)) | ||
((EventDispatcher<? extends T>) dispatcher).addListener(listener); | ||
}); | ||
} | ||
|
||
/** | ||
* <p>Registers all methods of the provided holder-instance that are annotated with {@link Listener} | ||
* as an {@link EventConsumer} (see: {@link #registerListener(Class, Object, EventConsumer)}</p> | ||
* | ||
* <p>Annotated methods need to have exactly one parameter which represents the event that should be listened for.</p> | ||
* | ||
* <p>An example listener method could look like this: | ||
* <blockquote><pre> | ||
* {@code @Events.Listener} | ||
* public void on(LifecycleEvent.Load.Post evt) { | ||
* // do something | ||
* } | ||
* </pre></blockquote></b> | ||
* | ||
* @param addon The addon/plugin/mod instance (can be used in {@link #unregisterListeners(Object)} | ||
* to unregister all listeners of this addon) | ||
* @param holder The instance that will be scanned for {@link Listener}-methods | ||
* @throws ListenerRegistrationException If an annotated method does not match the requirements to be used as an event-listener. | ||
*/ | ||
public static synchronized void registerListeners(Object addon, Object holder) throws ListenerRegistrationException { | ||
for (Method method : holder.getClass().getDeclaredMethods()) { | ||
if (!method.isAnnotationPresent(Listener.class)) continue; | ||
|
||
// sanity checks | ||
if (method.getParameterTypes().length != 1) { | ||
throw new ListenerRegistrationException("Failed to register listener-method '" + method + | ||
"': Method must have exactly one parameter!"); | ||
} | ||
|
||
if (!method.trySetAccessible()) { | ||
throw new ListenerRegistrationException("Failed to register listener-method '" + method + | ||
"': Method is not accessible!"); | ||
} | ||
|
||
Class<?> eventClass = method.getParameterTypes()[0]; | ||
registerListener(eventClass, addon, event -> method.invoke(holder, event)); | ||
} | ||
} | ||
|
||
/** | ||
* Unregisters an {@link EventConsumer} that has been previously registered with {@link #registerListener(Class, Object, EventConsumer)} | ||
* @param listener The listener that should be unregistered from all events | ||
*/ | ||
public static synchronized boolean unregisterListener(EventConsumer<?> listener) { | ||
boolean removed = false; | ||
for (EventDispatcher<?> dispatcher : dispatchers.values()) { | ||
removed |= dispatcher.removeListener(listener); | ||
} | ||
return removed; | ||
} | ||
|
||
/** | ||
* Unregisters all {@link EventConsumer}s that have been previously registered with the given addon-instance. | ||
* @param addon The addon instance whose listeners should be unregistered | ||
*/ | ||
public static synchronized void unregisterListeners(Object addon) { | ||
for (ListenerRegistration<?> registration : registrations) { | ||
Object registrationAddon = registration.getAddon(); | ||
if (registrationAddon != null && !registrationAddon.equals(addon)) continue; | ||
|
||
EventConsumer<?> listener = registration.getListener(); | ||
if (listener == null) continue; | ||
|
||
unregisterListener(listener); | ||
} | ||
|
||
// tidy up registrations list | ||
registrations.removeIf(registration -> registration.getListener() == null); | ||
} | ||
|
||
private static class ListenerRegistration<T> { | ||
private final Class<T> eventClass; | ||
private final WeakReference<Object> addon; | ||
private final WeakReference<EventConsumer<? super T>> listenerRef; | ||
|
||
public ListenerRegistration(Class<T> eventClass, Object addon, EventConsumer<? super T> listener) { | ||
this.eventClass = eventClass; | ||
this.addon = new WeakReference<>(addon); | ||
this.listenerRef = new WeakReference<>(listener); | ||
} | ||
|
||
public Class<T> getEventClass() { | ||
return eventClass; | ||
} | ||
|
||
public @Nullable Object getAddon() { | ||
return addon.get(); | ||
} | ||
|
||
public @Nullable EventConsumer<? super T> getListener() { | ||
return listenerRef.get(); | ||
} | ||
|
||
} | ||
|
||
/** | ||
* This Annotation represents a method that can be registered with {@link #registerListeners(Object, Object)}. | ||
* @see #registerListeners(Object, Object) | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
public @interface Listener {} | ||
|
||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/de/bluecolored/bluemap/api/events/ListenerRegistrationException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package de.bluecolored.bluemap.api.events; | ||
|
||
/** | ||
* Thrown if a listener registration was unsuccessful for some reason. | ||
*/ | ||
public class ListenerRegistrationException extends RuntimeException { | ||
|
||
public ListenerRegistrationException(final String message) { | ||
super(message); | ||
} | ||
|
||
} |
Oops, something went wrong.