From a7d83958ac59894691592501c054fb04770aec55 Mon Sep 17 00:00:00 2001 From: sk89q Date: Wed, 2 Apr 2014 18:53:06 -0700 Subject: [PATCH] Added event bus implementation. --- .../eventbus/AnnotatedSubscriberFinder.java | 69 +++++ .../worldedit/util/eventbus/EventBus.java | 261 ++++++++++++++++++ .../worldedit/util/eventbus/EventHandler.java | 105 +++++++ .../util/eventbus/MethodEventHandler.java | 81 ++++++ .../worldedit/util/eventbus/Subscribe.java | 42 +++ .../eventbus/SubscriberFindingStrategy.java | 43 +++ 6 files changed, 601 insertions(+) create mode 100644 src/main/java/com/sk89q/worldedit/util/eventbus/AnnotatedSubscriberFinder.java create mode 100644 src/main/java/com/sk89q/worldedit/util/eventbus/EventBus.java create mode 100644 src/main/java/com/sk89q/worldedit/util/eventbus/EventHandler.java create mode 100644 src/main/java/com/sk89q/worldedit/util/eventbus/MethodEventHandler.java create mode 100644 src/main/java/com/sk89q/worldedit/util/eventbus/Subscribe.java create mode 100644 src/main/java/com/sk89q/worldedit/util/eventbus/SubscriberFindingStrategy.java diff --git a/src/main/java/com/sk89q/worldedit/util/eventbus/AnnotatedSubscriberFinder.java b/src/main/java/com/sk89q/worldedit/util/eventbus/AnnotatedSubscriberFinder.java new file mode 100644 index 000000000..5415f2c4e --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/eventbus/AnnotatedSubscriberFinder.java @@ -0,0 +1,69 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.eventbus; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import java.lang.reflect.Method; + +/** + * A {@link SubscriberFindingStrategy} for collecting all event handler methods + * that are marked with the {@link Subscribe} annotation. + *

+ * Original for Guava, licensed under the Apache License, Version 2.0. + */ +class AnnotatedSubscriberFinder implements SubscriberFindingStrategy { + + /** + * {@inheritDoc} + * + * This implementation finds all methods marked with a {@link Subscribe} + * annotation. + */ + @Override + public Multimap, EventHandler> findAllSubscribers(Object listener) { + Multimap, EventHandler> methodsInListener = HashMultimap.create(); + Class clazz = listener.getClass(); + while (clazz != null) { + for (Method method : clazz.getMethods()) { + Subscribe annotation = method.getAnnotation(Subscribe.class); + method.setAccessible(true); + + if (annotation != null) { + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != 1) { + throw new IllegalArgumentException( + "Method " + method + " has @Subscribe annotation, but requires " + + parameterTypes.length + " arguments. Event handler methods " + + "must require a single argument."); + } + Class eventType = parameterTypes[0]; + EventHandler handler = new MethodEventHandler(annotation.priority(), listener, method); + methodsInListener.put(eventType, handler); + } + } + clazz = clazz.getSuperclass(); + } + + return methodsInListener; + } + +} \ No newline at end of file diff --git a/src/main/java/com/sk89q/worldedit/util/eventbus/EventBus.java b/src/main/java/com/sk89q/worldedit/util/eventbus/EventBus.java new file mode 100644 index 000000000..21e025a1e --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/eventbus/EventBus.java @@ -0,0 +1,261 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.eventbus; + +import com.google.common.base.Supplier; +import com.google.common.base.Throwables; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.collect.*; +import com.google.common.eventbus.DeadEvent; + +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Dispatches events to listeners, and provides ways for listeners to register + * themselves. + *

+ * This class is based on Guava's {@link EventBus} but priority is supported + * and events are dispatched at the time of call, rather than being queued up. + * This does allow dispatching during an in-progress dispatch. + *

+ * This implementation utilizes naive synchronization on all getter and setter + * methods. Dispatch does not occur when a lock has been acquired, however. + */ +public class EventBus { + + private final Logger logger = Logger.getLogger(EventBus.class.getCanonicalName()); + + private final SetMultimap, EventHandler> handlersByType = + Multimaps.newSetMultimap(new HashMap, Collection>(), + new Supplier>() { + @Override + public Set get() { + return newHandlerSet(); + } + }); + + /** + * Strategy for finding handler methods in registered objects. Currently, + * only the {@link AnnotatedSubscriberFinder} is supported, but this is + * encapsulated for future expansion. + */ + private final SubscriberFindingStrategy finder = new AnnotatedSubscriberFinder(); + + /** + * A thread-safe cache for flattenHierarchy(). The Class class is immutable. + */ + private Cache, Set>> flattenHierarchyCache = + CacheBuilder.newBuilder() + .weakKeys() + .build(new CacheLoader, Set>>() { + @Override + public Set> load(Class concreteClass) throws Exception { + List> parents = Lists.newLinkedList(); + Set> classes = Sets.newHashSet(); + + parents.add(concreteClass); + + while (!parents.isEmpty()) { + Class clazz = parents.remove(0); + classes.add(clazz); + + Class parent = clazz.getSuperclass(); + if (parent != null) { + parents.add(parent); + } + + Collections.addAll(parents, clazz.getInterfaces()); + } + + return classes; + } + }); + + /** + * Registers the given handler for the given class to receive events. + * + * @param clazz the event class to register + * @param handler the handler to register + */ + public synchronized void subscribe(Class clazz, EventHandler handler) { + checkNotNull(clazz); + checkNotNull(handler); + handlersByType.put(clazz, handler); + } + + /** + * Registers the given handler for the given class to receive events. + * + * @param handlers a map of handlers + */ + public synchronized void subscribeAll(Multimap, EventHandler> handlers) { + checkNotNull(handlers); + handlersByType.putAll(handlers); + } + + /** + * Unregisters the given handler for the given class. + * + * @param clazz the class + * @param handler the handler + */ + public synchronized void unsubscribe(Class clazz, EventHandler handler) { + checkNotNull(clazz); + checkNotNull(handler); + handlersByType.remove(clazz, handler); + } + + /** + * Unregisters the given handlers. + * + * @param handlers a map of handlers + */ + public synchronized void unsubscribeAll(Multimap, EventHandler> handlers) { + checkNotNull(handlers); + for (Map.Entry, Collection> entry : handlers.asMap().entrySet()) { + Set currentHandlers = getHandlersForEventType(entry.getKey()); + Collection eventMethodsInListener = entry.getValue(); + + if (currentHandlers != null &&!currentHandlers.containsAll(entry.getValue())) { + currentHandlers.removeAll(eventMethodsInListener); + } + } + } + + /** + * Registers all handler methods on {@code object} to receive events. + * Handler methods are selected and classified using this EventBus's + * {@link SubscriberFindingStrategy}; the default strategy is the + * {@link AnnotatedSubscriberFinder}. + * + * @param object object whose handler methods should be registered. + */ + public void register(Object object) { + subscribeAll(finder.findAllSubscribers(object)); + } + + /** + * Unregisters all handler methods on a registered {@code object}. + * + * @param object object whose handler methods should be unregistered. + * @throws IllegalArgumentException if the object was not previously registered. + */ + public void unregister(Object object) { + unsubscribeAll(finder.findAllSubscribers(object)); + } + + /** + * Posts an event to all registered handlers. This method will return + * successfully after the event has been posted to all handlers, and + * regardless of any exceptions thrown by handlers. + * + *

If no handlers have been subscribed for {@code event}'s class, and + * {@code event} is not already a {@link DeadEvent}, it will be wrapped in a + * DeadEvent and reposted. + * + * @param event event to post. + */ + public void post(Object event) { + List dispatching = new ArrayList(); + + synchronized (this) { + Set> dispatchTypes = flattenHierarchy(event.getClass()); + + for (Class eventType : dispatchTypes) { + Set wrappers = getHandlersForEventType(eventType); + + if (wrappers != null && !wrappers.isEmpty()) { + dispatching.addAll(wrappers); + } + } + } + + Collections.sort(dispatching); + + for (EventHandler handler : dispatching) { + dispatch(event, handler); + } + } + + /** + * Dispatches {@code event} to the handler in {@code handler}. This method + * is an appropriate override point for subclasses that wish to make + * event delivery asynchronous. + * + * @param event event to dispatch. + * @param handler handler that will call the handler. + */ + protected void dispatch(Object event, EventHandler handler) { + try { + handler.handleEvent(event); + } catch (InvocationTargetException e) { + logger.log(Level.SEVERE, + "Could not dispatch event: " + event + " to handler " + handler, e); + } + } + + /** + * Retrieves a mutable set of the currently registered handlers for + * {@code type}. If no handlers are currently registered for {@code type}, + * this method may either return {@code null} or an empty set. + * + * @param type type of handlers to retrieve. + * @return currently registered handlers, or {@code null}. + */ + synchronized Set getHandlersForEventType(Class type) { + return handlersByType.get(type); + } + + /** + * Creates a new Set for insertion into the handler map. This is provided + * as an override point for subclasses. The returned set should support + * concurrent access. + * + * @return a new, mutable set for handlers. + */ + protected synchronized Set newHandlerSet() { + return new HashSet(); + } + + /** + * Flattens a class's type hierarchy into a set of Class objects. The set + * will include all superclasses (transitively), and all interfaces + * implemented by these superclasses. + * + * @param concreteClass class whose type hierarchy will be retrieved. + * @return {@code clazz}'s complete type hierarchy, flattened and uniqued. + */ + Set> flattenHierarchy(Class concreteClass) { + try { + return flattenHierarchyCache.get(concreteClass); + } catch (ExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + } + +} diff --git a/src/main/java/com/sk89q/worldedit/util/eventbus/EventHandler.java b/src/main/java/com/sk89q/worldedit/util/eventbus/EventHandler.java new file mode 100644 index 000000000..1903d80e3 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/eventbus/EventHandler.java @@ -0,0 +1,105 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.eventbus; + +import java.lang.reflect.InvocationTargetException; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Event handler object for {@link EventBus} that is able to dispatch + * an event. + *

+ * Original for Guava, licensed under the Apache License, Version 2.0. + */ +public abstract class EventHandler implements Comparable { + + public enum Priority { + VERY_EARLY, + EARLY, + NORMAL, + LATE, + VERY_LATE + } + + private final Priority priority; + + /** + * Create a new event handler. + * + * @param priority the priority + */ + protected EventHandler(Priority priority) { + checkNotNull(priority); + this.priority = priority; + } + + /** + * Get the priority. + * + * @return the priority + */ + public Priority getPriority() { + return priority; + } + + /** + * Dispatch the given event. + *

+ * Subclasses should override {@link #dispatch(Object)}. + * + * @param event the event + * @throws InvocationTargetException thrown if an exception is thrown during dispatch + */ + public final void handleEvent(Object event) throws InvocationTargetException { + try { + dispatch(event); + } catch (Throwable t) { + throw new InvocationTargetException(t); + } + } + + /** + * Dispatch the event. + * + * @param event the event object + * @throws Exception an exception that may be thrown + */ + public abstract void dispatch(Object event) throws Exception; + + @Override + public int compareTo(EventHandler o) { + return getPriority().ordinal() - o.getPriority().ordinal(); + } + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object obj); + + @Override + public String toString() { + return "EventHandler{" + + "priority=" + priority + + '}'; + } + +} diff --git a/src/main/java/com/sk89q/worldedit/util/eventbus/MethodEventHandler.java b/src/main/java/com/sk89q/worldedit/util/eventbus/MethodEventHandler.java new file mode 100644 index 000000000..40f087b8c --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/eventbus/MethodEventHandler.java @@ -0,0 +1,81 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.eventbus; + +import java.lang.reflect.Method; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Invokes a {@link Method} to dispatch an event. + */ +public class MethodEventHandler extends EventHandler { + + private final Object object; + private final Method method; + + /** + * Create a new event handler. + * + * @param priority the priority + * @param method the method + */ + public MethodEventHandler(Priority priority, Object object, Method method) { + super(priority); + checkNotNull(method); + this.object = object; + this.method = method; + } + + /** + * Get the method. + * + * @return the method + */ + public Method getMethod() { + return method; + } + + @Override + public void dispatch(Object event) throws Exception { + method.invoke(object, event); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MethodEventHandler that = (MethodEventHandler) o; + + if (!method.equals(that.method)) return false; + if (object != null ? !object.equals(that.object) : that.object != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = object != null ? object.hashCode() : 0; + result = 31 * result + method.hashCode(); + return result; + } +} diff --git a/src/main/java/com/sk89q/worldedit/util/eventbus/Subscribe.java b/src/main/java/com/sk89q/worldedit/util/eventbus/Subscribe.java new file mode 100644 index 000000000..41b8a4377 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/eventbus/Subscribe.java @@ -0,0 +1,42 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.eventbus; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; + +/** + * Used to mark methods as event handlers. + */ +@Retention(RUNTIME) +@Target({METHOD}) +public @interface Subscribe { + + /** + * The priority as far as order of dispatching is concerned. + * + * @return the priority + */ + EventHandler.Priority priority() default EventHandler.Priority.NORMAL; + +} diff --git a/src/main/java/com/sk89q/worldedit/util/eventbus/SubscriberFindingStrategy.java b/src/main/java/com/sk89q/worldedit/util/eventbus/SubscriberFindingStrategy.java new file mode 100644 index 000000000..78e470a6a --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/eventbus/SubscriberFindingStrategy.java @@ -0,0 +1,43 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.eventbus; + +import com.google.common.collect.Multimap; + +/** + * A method for finding event handler methods in objects, for use by + * {@link EventBus}. + */ +interface SubscriberFindingStrategy { + + /** + * Finds all suitable event handler methods in {@code source}, organizes them + * by the type of event they handle, and wraps them in {@link EventHandler}s. + * + * @param source object whose handlers are desired. + * @return EventHandler objects for each handler method, organized by event + * type. + * + * @throws IllegalArgumentException if {@code source} is not appropriate for + * this strategy (in ways that this interface does not define). + */ + Multimap, EventHandler> findAllSubscribers(Object source); + +} \ No newline at end of file