Skip to content

Commit

Permalink
First draft of events
Browse files Browse the repository at this point in the history
  • Loading branch information
TBlueF committed May 20, 2024
1 parent ec97711 commit d83c40d
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/main/java/de/bluecolored/bluemap/api/BlueMapAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import de.bluecolored.bluemap.api.events.APIEvent;
import de.bluecolored.bluemap.api.events.EventDispatcher;
import de.bluecolored.bluemap.api.events.Events;
import de.bluecolored.bluemap.api.plugin.Plugin;
import org.jetbrains.annotations.ApiStatus;

Expand All @@ -46,6 +49,9 @@
@SuppressWarnings({"unused", "UnusedReturnValue"})
public abstract class BlueMapAPI {

private static final EventDispatcher<APIEvent.Enable> ENABLE_DISPATCHER = Events.getDispatcher(APIEvent.Enable.class);
private static final EventDispatcher<APIEvent.Disable> DISABLE_DISPATCHER = Events.getDispatcher(APIEvent.Disable.class);

@SuppressWarnings("unused")
private static final String VERSION, GIT_HASH;
static {
Expand Down Expand Up @@ -210,6 +216,12 @@ protected static synchronized boolean registerInstance(BlueMapAPI instance) thro
}
}

try {
ENABLE_DISPATCHER.dispatch(new APIEvent.Enable(BlueMapAPI.instance));
} catch (Exception ex) {
thrownExceptions.add(ex);
}

return throwAsOne(thrownExceptions);
}

Expand All @@ -233,6 +245,12 @@ protected static synchronized boolean unregisterInstance(BlueMapAPI instance) th
}
}

try {
DISABLE_DISPATCHER.dispatch(new APIEvent.Disable(BlueMapAPI.instance));
} catch (Exception ex) {
thrownExceptions.add(ex);
}

BlueMapAPI.instance = null;

return throwAsOne(thrownExceptions);
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/de/bluecolored/bluemap/api/events/APIEvent.java
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 src/main/java/de/bluecolored/bluemap/api/events/EventConsumer.java
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;

}
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 src/main/java/de/bluecolored/bluemap/api/events/Events.java
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 {}

}
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);
}

}
Loading

0 comments on commit d83c40d

Please sign in to comment.