/*
* 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.events.manager;
import com.jcwhatever.nucleus.Nucleus;
import com.jcwhatever.nucleus.collections.observer.agent.AgentHashMap;
import com.jcwhatever.nucleus.collections.observer.agent.AgentMap;
import com.jcwhatever.nucleus.collections.observer.subscriber.SubscriberMultimap;
import com.jcwhatever.nucleus.collections.observer.subscriber.SubscriberSetMultimap;
import com.jcwhatever.nucleus.collections.timed.TimedHashSet;
import com.jcwhatever.nucleus.mixins.ICancellable;
import com.jcwhatever.nucleus.mixins.IDisposable;
import com.jcwhatever.nucleus.mixins.IPluginOwned;
import com.jcwhatever.nucleus.utils.CollectionUtils;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.TimeScale;
import com.jcwhatever.nucleus.utils.observer.ISubscriber;
import com.jcwhatever.nucleus.utils.observer.event.EventAgent;
import com.jcwhatever.nucleus.utils.observer.event.EventSubscriber;
import com.jcwhatever.nucleus.utils.observer.event.IEventSubscriber;
import com.jcwhatever.nucleus.utils.observer.event.IEventWrapper;
import com.jcwhatever.nucleus.utils.observer.update.IUpdateSubscriber;
import com.jcwhatever.nucleus.utils.observer.update.UpdateAgent;
import org.bukkit.Bukkit;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Nucleus event manager.
*
* <p>Nucleus events are primarily intended for use in a mostly self contained
* system with many contexts that benefit from each having their own event manager. This
* can reduce code and the number of checks by guaranteeing event handlers in a specific event
* manager are only called in response to the managers specific context. In can also potentially
* improve performance by reducing the number of event handlers called to the ones that are subscribed
* to the specific context of the event manager. It can also harm performance if not used properly.</p>
*
* <p>The Nucleus event manager can take any type as an event including Bukkit events.
* It can also have a parent manager that receives calls made to the child manager.
* By default, the global Nucleus event manager is the parent manager, however
* a different parent manager or none at all can be set.</p>
*
* <p>The global Nucleus event manager also receives certain Bukkit events so Nucleus event
* handlers can be used for those Bukkit events.</p>
*
* <p>You are encouraged to use the event managers {@link #callBukkit} method to call your custom
* Bukkit events. This will first call the event using Bukkit's event system, then again on the Nucleus
* event manager to allow its event subscribers to handle the event.</p>
*
* <p>Event managers cannot receive the same event more than once. This is because of the combination of event
* forwarding to specific manager contexts and event bubbling. It is possible to forward an event from an event
* manager that is higher in the hierarchy to a manager that is lower, which would then bubble to the
* manager that forwarded the event in the first place. To prevent this managers will drop calls to event
* instances that they have already called.</p>
*
* <p>In order to prevent undesired dropping of events, the event type should not override the equals method, or
* if it does, should compare instances using ==.</p>
*
* <p>Note: The event manager runs events slightly different from Bukkit events. When an event is cancelled,
* all event handlers that would have been called after are not called unless they have been set to be invoked
* even if the event is cancelled. If a handler un-cancels an event, any handler that was skipped is then run.</p>
*/
public class EventManager implements IPluginOwned, IDisposable {
private static final SubscriberMultimap<Plugin, IEventSubscriber> _pluginEventMap
= new SubscriberSetMultimap<>(30, 10);
private static final SubscriberMultimap<Plugin, IUpdateSubscriber> _pluginCallMap
= new SubscriberSetMultimap<>(7, 3);
/**
* Remove all registered event handlers and listeners from
* the specified plugin from all event managers.
*
* <p>Automatically invoked when a plugin is disabled.</p>
*
* @param plugin The plugin.
*/
public static void unregisterPlugin(Plugin plugin) {
PreCon.notNull(plugin);
synchronized (_pluginEventMap) {
Collection<IEventSubscriber> eventSubscribers = _pluginEventMap.removeAll(plugin);
for (IEventSubscriber subscriber : eventSubscribers) {
subscriber.dispose();
}
}
synchronized (_pluginCallMap) {
Collection<IUpdateSubscriber> callSubscribers = _pluginCallMap.removeAll(plugin);
for (IUpdateSubscriber subscriber : callSubscribers) {
subscriber.dispose();
}
}
}
private final Plugin _plugin;
private final EventManager _parent;
private final AgentMap<Class<?>, EventAgent> _eventAgents = new AgentHashMap<>(10);
private final Map<IEventListener, ListenerInfo> _listeners = new HashMap<>(10);
private final TimedHashSet<Object> _calledEvents;
private final UpdateAgent<Object> _callAgent = new UpdateAgent<>();
private final Object _sync = new Object();
private volatile boolean _isDisposed;
/**
* Constructor.
*
* <p>Create a new event manager using the global event manager as parent.</p>
*
* @param plugin The owning plugin.
*/
public EventManager(Plugin plugin) {
this(plugin, Nucleus.getEventManager());
}
/**
* Constructor.
*
* @param plugin The owning plugin.
* @param parent The parent event manager. The parent receives all
* event calls the child receives.
*/
public EventManager(Plugin plugin, @Nullable EventManager parent) {
PreCon.notNull(plugin);
_plugin = plugin;
_parent = parent;
_calledEvents = new TimedHashSet<>(plugin, 10, 1, TimeScale.TICKS);
}
@Override
public Plugin getPlugin() {
return _plugin;
}
/**
* Register an event subscriber.
*
* @param plugin The subscribers owning plugin.
* @param event The event to subscribe to.
* @param subscriber The event subscriber.
*
* @param <T> The event type.
*/
public <T> void register(Plugin plugin, Class<T> event, IEventSubscriber<T> subscriber) {
PreCon.notNull(event);
PreCon.notNull(subscriber);
if (isDisposed())
throw new RuntimeException("Cannot use a disposed event manager.");
EventAgent agent = getEventAgent(event, true);
agent.addSubscriber(subscriber);
_pluginEventMap.put(plugin, subscriber);
}
/**
* Register an event listener. Methods from the listener annotated
* with {@link EventMethod} are registered as event subscribers.
*
* <p>The method must have only one parameter whose type is that of
* the event it subscribes to.</p>
*
* <p>The method visibility does not matter. It can be private, protected
* or public.</p>
*
* @param listener The listener to register.
*/
public void register(IEventListener listener) {
PreCon.notNull(listener);
if (isDisposed())
throw new RuntimeException("Cannot use a disposed event manager.");
List<IEventSubscriber> subscribers = extractSubscribers(listener);
if (subscribers.isEmpty())
return;
ListenerInfo info = new ListenerInfo(this, listener, subscribers);
synchronized (_sync) {
_listeners.put(listener, info);
}
for (IEventSubscriber subscriber : subscribers) {
EventMethodWrapper<?> wrapper = (EventMethodWrapper<?>)subscriber;
EventAgent agent = getEventAgent(wrapper.event, true);
//noinspection unchecked
agent.addSubscriber(subscriber);
}
_pluginEventMap.putAll(listener.getPlugin(), subscribers);
}
/**
* Unregister an event subscriber.
*
* @param subscriber The event subscriber.
*/
public void unregister(IEventSubscriber subscriber) {
PreCon.notNull(subscriber);
if (_eventAgents.unregisterAll(subscriber)) {
synchronized (_pluginEventMap) {
CollectionUtils.removeValue(_pluginEventMap, subscriber);
}
}
}
/**
* Unregister an event listener and all its event
* subscribers.
*
* @param listener The listener to unregister.
*/
public void unregister(IEventListener listener) {
PreCon.notNull(listener);
synchronized (_sync) {
ListenerInfo info = _listeners.remove(listener);
if (info == null)
return;
// unregister listeners subscribers from agents
info.unregister();
}
}
/**
* Calls a Bukkit event using Bukkits event manager, then calls the
* event on the {@link EventManager} instance.
*
* @param caller Optional source of the event.
* @param event The event.
*
* @param <T> The event type.
*
* @return The event.
*/
public <T extends Event> T callBukkit(@Nullable Object caller, T event) {
if (isDisposed())
throw new RuntimeException("Cannot use a disposed event manager.");
Bukkit.getPluginManager().callEvent(event);
return call(caller, event);
}
/**
* Call an event.
*
* @param caller Optional source of the event.
* @param event The event
* .
* @param <T> The event type.
*
* @return The event.
*/
public <T> T call(@Nullable Object caller, T event) {
if (isDisposed())
throw new RuntimeException("Cannot use a disposed event manager.");
// prevent redirected events from bubbling back to
// an event manager its was already called on.
synchronized (_sync) {
if (_calledEvents.contains(event))
return event;
_calledEvents.add(event);
}
// call event on parent first
if (_parent != null) {
_parent.call(caller, event);
}
_callAgent.update(event);
EventAgent agent = getEventAgent(event.getClass(), false);
if (agent == null)
return event;
// check for Bukkit cancellable event
if (event instanceof Cancellable) {
agent.call(caller, new BukkitEventWrapper<T>(event));
}
else {
agent.call(caller, event);
}
return event;
}
/**
* Attach an {@link IUpdateSubscriber} that receives all events called.
*
* @param plugin The subscribers owning plugin.
* @param subscriber The subscriber.
*
* @return Self for chaining.
*/
public EventManager onCall(Plugin plugin, IUpdateSubscriber<?> subscriber) {
PreCon.notNull(subscriber);
if (isDisposed())
throw new RuntimeException("Cannot use a disposed event manager.");
_callAgent.addSubscriber(subscriber);
_pluginCallMap.put(plugin, subscriber);
return this;
}
@Override
public boolean isDisposed() {
return _isDisposed;
}
@Override
public void dispose() {
if (_isDisposed)
return;
_eventAgents.dispose();
_callAgent.dispose();
_isDisposed = true;
}
/*
* Extract all valid event subscribers from a listener.
*/
private List<IEventSubscriber> extractSubscribers(IEventListener listener) {
synchronized (_sync) {
if (_listeners.containsKey(listener))
throw new RuntimeException("Listener already registered.");
}
// get all methods from listener
Method[] methods = listener.getClass().getDeclaredMethods();
List<IEventSubscriber> subscribers = new ArrayList<>(methods.length);
for (Method method : methods) {
EventMethod annotation = method.getAnnotation(EventMethod.class);
if (annotation == null)
continue;
// event handlers must have exactly one parameter
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes == null || paramTypes.length != 1)
continue;
Class<?> eventClass = paramTypes[0];
IEventSubscriber<?> wrapper = new EventMethodWrapper<>(listener, eventClass, method, annotation);
subscribers.add(wrapper);
}
return subscribers;
}
/*
* Get the event agent for an event type.
*/
private EventAgent getEventAgent(Class<?> eventClass, boolean create) {
synchronized (_sync) {
EventAgent agent = _eventAgents.get(eventClass);
if (agent == null && create) {
agent = new EventAgent();
_eventAgents.put(eventClass, agent);
}
return agent;
}
}
/*
* Stores the event subscribers extracted from a listener.
*/
private static class ListenerInfo {
final EventManager manager;
final IEventListener listener;
final List<IEventSubscriber> subscribers;
ListenerInfo(EventManager manager, IEventListener listener, List<IEventSubscriber> methodSubscribers) {
this.manager = manager;
this.listener = listener;
this.subscribers = new ArrayList<>(methodSubscribers);
}
/**
* Unregister the listener from the handler collections.
*/
public void unregister() {
for (ISubscriber subscriber: subscribers) {
subscriber.dispose();
}
subscribers.clear();
}
}
/*
* An event subscriber wrapper for a method extracted from an event listener.
*/
private static class EventMethodWrapper<E> extends EventSubscriber<E> implements IEventSubscriber<E> {
final Object listener;
final Class<?> event;
final Method method;
final EventMethod annotation;
EventMethodWrapper(Object listener, Class<?> event, Method method, EventMethod annotation) {
this.listener = listener;
this.event = event;
this.method = method;
this.annotation = annotation;
method.setAccessible(true);
setPriority(annotation.priority());
setInvokedForCancelled(annotation.invokeForCancelled());
}
@Override
public void onEvent(@Nullable Object caller, E event) {
try {
this.method.invoke(this.listener, event);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
/*
* A wrapper for Bukkit events used to make the events cancel methods
* available.
*/
private static class BukkitEventWrapper<E> implements IEventWrapper<E>, ICancellable {
final E event;
public BukkitEventWrapper(E event) {
this.event = event;
}
@Override
public boolean isCancelled() {
return event instanceof Cancellable && ((Cancellable) event).isCancelled();
}
@Override
public void setCancelled(boolean isCancelled) {
if (event instanceof Cancellable)
((Cancellable) event).setCancelled(isCancelled);
}
@Override
public E getEvent() {
return event;
}
}
}