Bleeding: Add ListenerManager (experimental).

The ListenerManager just allows to register Listener objects, but also
allows to register before other or certain other listeners. This might
be an interesting addition for compatibility add-ons.
This commit is contained in:
asofold 2012-11-07 12:56:25 +01:00
parent affdffe1d8
commit 19d6ed4210
3 changed files with 360 additions and 3 deletions

View File

@ -39,6 +39,7 @@ import fr.neatmonster.nocheatplus.config.ConfPaths;
import fr.neatmonster.nocheatplus.config.ConfigFile;
import fr.neatmonster.nocheatplus.config.ConfigManager;
import fr.neatmonster.nocheatplus.config.DefaultConfig;
import fr.neatmonster.nocheatplus.event.ListenerManager;
import fr.neatmonster.nocheatplus.hooks.NCPExemptionManager;
import fr.neatmonster.nocheatplus.metrics.Metrics;
import fr.neatmonster.nocheatplus.metrics.Metrics.Graph;
@ -174,12 +175,16 @@ public class NoCheatPlus extends JavaPlugin implements Listener, NoCheatPlusAPI
* use.
*/
protected List<CommandProtectionEntry> changedCommands = null;
protected final ListenerManager listenerManager = new ListenerManager(this, false);
@Override
public void addComponent(final Object obj) {
if (obj instanceof Listener) {
final Listener listener = (Listener) obj;
Bukkit.getPluginManager().registerEvents(listener, this);
// Bukkit.getPluginManager().registerEvents(listener, this);
listenerManager.registerAllEventHandlers(listener, "NoCheatPlus");
listeners.add(listener);
}
if (obj instanceof INotifyReload) {
@ -193,7 +198,10 @@ public class NoCheatPlus extends JavaPlugin implements Listener, NoCheatPlusAPI
@Override
public void removeComponent(final Object obj) {
listeners.remove(obj);
if (obj instanceof Listener){
listeners.remove(obj);
listenerManager.remove((Listener) obj);
}
notifyReload.remove(obj);
dataMan.removeComponent(obj);
}
@ -235,6 +243,10 @@ public class NoCheatPlus extends JavaPlugin implements Listener, NoCheatPlusAPI
// Restore changed commands.
undoCommandChanges();
// Remove listener references.
listenerManager.setRegisterDirectly(false);
listenerManager.clear();
// Tell the server administrator the we finished unloading NoCheatPlus.
CheckUtils.logInfo("[NoCheatPlus] Version " + pdfFile.getVersion() + " is disabled.");
@ -281,7 +293,10 @@ public class NoCheatPlus extends JavaPlugin implements Listener, NoCheatPlusAPI
BlockProperties.applyConfig(config, ConfPaths.COMPATIBILITY_BLOCKS); // Temp probably,
// List the events listeners and register.
Bukkit.getPluginManager().registerEvents(this, this);
// Bukkit.getPluginManager().registerEvents(this, this);
listenerManager.setRegisterDirectly(true);
listenerManager.registerAllWithBukkit();
listenerManager.registerAllEventHandlers(this, "NoCheatPlus");
for (final Object obj : new Object[]{
NCPExemptionManager.getListener(),
dataMan,

View File

@ -0,0 +1,175 @@
package fr.neatmonster.nocheatplus.event;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventException;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.plugin.EventExecutor;
import org.bukkit.plugin.Plugin;
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
/**
* listener registered for one event only. Allows to delegate to other registered listeners.
* @author mc_dev
*
* @param <E>
*/
public class GenericListener<E extends Event> implements Listener, EventExecutor {
public static class MethodEntry{
/**
* beforeTag overrides afterTag
* @author mc_dev
*
*/
public static class MethodOrder{
public final String beforeTag;
/**
* You can use regular expressions for beforeTag, so it will be checked from position 0 on.<br>
* You can specify "*" for "all other tags" to add it in front.
* @param beforeTag
*/
public MethodOrder(String beforeTag){
this.beforeTag = beforeTag;
}
}
// One method
// TODO
public final Object listener;
public final Method method;
public final boolean ignoreCancelled;
/**
* This allows setting some information about where this listener comes from,
* also allowing to register before listeners with other tags. The default plugin should be null.
*/
public final String tag;
public final MethodOrder order;
public MethodEntry(Object listener, Method method, boolean ignoreCancelled, String tag, MethodOrder order){
this.listener = listener;
this.method = method;
this.ignoreCancelled = ignoreCancelled;
this.tag = tag;
this.order = order;
}
}
////////////////
// Instance
////////////////
protected final Class<E> clazz;
protected MethodEntry[] entries = new MethodEntry[0];
/** If the event type implements Cancellable, is set once only, null means unknsown (TODO) */
protected final boolean isCancellable;
/** Event priority, for debugging purposes. Note that the factories seem to have to use another variable name. */
protected final EventPriority priority;
private boolean registered = false;
public GenericListener(final Class<E> clazz, final EventPriority priority) {
this.clazz = clazz;
this.priority = priority;
isCancellable = clazz.isInstance(Cancellable.class);
}
@Override
public final void execute(final Listener listener, final Event event){
// TODO: profiling option !
final Cancellable cancellable = isCancellable ? (Cancellable) event : null;
final MethodEntry[] entries = this.entries;
for (int i = 0; i < entries.length ; i++){
final MethodEntry entry = entries[i];
try {
if (!isCancellable || !entry.ignoreCancelled || !cancellable.isCancelled()) entry.method.invoke(entry.listener, event);
} catch (Throwable t) {
// IllegalArgumentException IllegalAccessException InvocationTargetException
final EventException e = new EventException(t, "GenericListener<" + clazz.getName() +"> @" + priority +" encountered an exception for " + entry.listener.getClass().getName() + " with method " + entry.method.toGenericString());
// TODO: log it / more details!
if (event.isAsynchronous()) CheckUtils.scheduleOutput(e);
else CheckUtils.logSevere(e);
}
}
}
public void register(Plugin plugin) {
if (registered) return;
Bukkit.getPluginManager().registerEvent(clazz, this, priority, this, plugin, false);
registered = true;
}
public boolean isRegistered() {
return registered;
}
public void addMethodEntry(final MethodEntry entry){
// TODO: maybe optimize later.
// MethodOrder: the new enties order will be compared versus the old entries tags, not the other way round !).
int insertion = -1;
if (entry.order != null){
if (entry.order.beforeTag != null){
if ("*".equals(entry.order.beforeTag)){
insertion = 0;
}
else{
for (int i = 0; i < entries.length; i++){
MethodEntry other = entries[i];
if (other.order != null){
if (other.tag.matches(entry.order.beforeTag)){
insertion = i;
break;
}
}
}
}
}
}
if (insertion == entries.length || insertion == -1){
entries = Arrays.copyOf(entries, entries.length + 1);
entries[entries.length - 1] = entry;
}
else{
MethodEntry[] newEntries = new MethodEntry[entries.length + 1];
for (int i = 0; i < entries.length + 1; i ++ ){
if (i < insertion) newEntries[i] = entries[i];
else if (i == insertion) newEntries[i] = entry;
else{
// i > insertion
newEntries[i] = entries[i - 1];
}
}
entries = newEntries;
}
}
/**
* TODO: more methods for tags ? (....return type ?)
* @param listener
*/
public void remove(Listener listener) {
List<MethodEntry> keep = new ArrayList<MethodEntry>(entries.length);
for (MethodEntry entry : entries){
if (entry.listener != listener) keep.add(entry);
}
if (keep.size() != entries.length){
entries = new MethodEntry[keep.size()];
keep.toArray(entries);
}
}
}

View File

@ -0,0 +1,167 @@
package fr.neatmonster.nocheatplus.event;
import java.lang.reflect.Method;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
import fr.neatmonster.nocheatplus.event.GenericListener.MethodEntry;
import fr.neatmonster.nocheatplus.event.GenericListener.MethodEntry.MethodOrder;
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
/**
* This class allows to register event-listeners which will all be called form within one event handler per event+priority combination.<br>
* In future this will also allow to control registration order, to ensure receiving events before or after some other listeners or plugins.<br>
* @author mc_dev
*
*/
public class ListenerManager {
protected Map<Class<? extends Event>, EnumMap<EventPriority, GenericListener<?>>> map = new HashMap<Class<? extends Event>, EnumMap<EventPriority,GenericListener<?>>>();
private final Plugin plugin;
private boolean registerDirectly;
public ListenerManager(Plugin plugin){
this(plugin, false);
}
public ListenerManager(Plugin plugin, boolean registerDirectly){
this.plugin = plugin;
this.registerDirectly = true;
}
/**
* Probably put to protected later.
* @param clazz
* @param priority
* @return
*/
public <E extends Event> GenericListener<E> getListener(Class<E> clazz, EventPriority priority){
EnumMap<EventPriority, GenericListener<?>> prioMap = map.get(clazz);
if (prioMap == null){
prioMap = new EnumMap<EventPriority, GenericListener<?>>(EventPriority.class);
map.put(clazz, prioMap);
}
@SuppressWarnings("unchecked")
GenericListener<E> listener = (GenericListener<E>) prioMap.get(priority);
if (listener == null){
listener = new GenericListener<E>(clazz, priority);
prioMap.put(priority, listener);
}
if (registerDirectly && !listener.isRegistered()) listener.register(plugin);
return listener;
}
public Plugin getPlugin() {
return plugin;
}
public boolean isRegisterDirectly() {
return registerDirectly;
}
public void setRegisterDirectly(boolean registerDirectly) {
this.registerDirectly = registerDirectly;
}
/**
* Register all yet unregistered generic listeners with the PluginManager.<br>
* NOTE: This does not set registerDirectly.
*/
public void registerAllWithBukkit(){
for (final EnumMap<EventPriority, GenericListener<?>> prioMap : map.values()){
for (final GenericListener<?> listener : prioMap.values()){
if (!listener.isRegistered()) listener.register(plugin);
}
}
}
/**
* Clear internal mappings.
*/
public void clear(){
map.clear();
}
/**
* This registers all declared methods that have the @EventHandler annotation.<br>
* NOTE: Does not do any super class checking.
* @param listener
* @param tag Identifier for the registering plugin / agent, null is not discouraged, but null entries are ignored concerning sortin order.
*/
public void registerAllEventHandlers(Listener listener, String tag){
registerAllEventHandlers(listener, tag, null);
}
/**
* This registers all methods that have the @EventHandler annotation.<br>
* NOTE: Does not do any super class checking.
* @param listener
* @param tag Identifier for the registering plugin / agent, null is not discouraged, but null entries are ignored concerning sortin order.
* @param order Allows to register before other tags or just first. Expect MethodOrder to change in near future. The method order of already registered methods will not be compared to.
*/
public void registerAllEventHandlers(Listener listener, String tag, MethodOrder order){
Class<?> clazz = listener.getClass();
Set<Method> allMethods = new HashSet<Method>();
for (Method method : clazz.getMethods()){
allMethods.add(method);
}
for (Method method : clazz.getDeclaredMethods()){
allMethods.add(method);
}
for (Method method : allMethods){
EventHandler anno = method.getAnnotation(EventHandler.class);
if (anno == null) continue;
if (!method.isAccessible()){
// Try to make it accessible.
try{
method.setAccessible(true);
} catch (SecurityException e){
CheckUtils.logWarning("[ListenerManager] Can not set method accessible: " + method.toGenericString() +" registered in " + clazz.getName()+ ", ignoring it!");
}
}
Class<?>[] argTypes = method.getParameterTypes();
if (argTypes.length != 1){
CheckUtils.logWarning("[ListenerManager] Bad method signature (number of arguments not 1): " + method.toGenericString() +" registered in " + clazz.getName()+ ", ignoring it!");
continue;
}
Class<?> eventType = argTypes[0];
if (!Event.class.isAssignableFrom(eventType)){
CheckUtils.logWarning("[ListenerManager] Bad method signature (argument does not extend Event): " + method.toGenericString() +" registered in " + clazz.getName()+ ", ignoring it!");
continue;
}
Class<? extends Event> checkedEventType = eventType.asSubclass(Event.class);
getListener(checkedEventType, anno.priority()).addMethodEntry(new MethodEntry(listener, method, anno.ignoreCancelled(), tag, order));
}
}
protected static boolean extendsEvent(Class<?> eventType) {
Class<?> superClass = eventType.getSuperclass();
while (superClass != null && superClass != Object.class){
if (superClass == Event.class) return true;
superClass = superClass.getSuperclass();
}
return false;
}
/**
* TODO: more methods for tags ? (+ return something?).
* @param listener
*/
public void remove(Listener listener) {
for (EnumMap<EventPriority, GenericListener<?>> prioMap : map.values()){
for (GenericListener<?> gl : prioMap.values()){
gl.remove(listener);
}
}
}
}