diff --git a/api/pom.xml b/api/pom.xml index 0eee082c..b5834e96 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -31,6 +31,12 @@ 1.7.14 compile + + net.md-5 + bungeecord-event + ${project.version} + compile + org.yaml snakeyaml diff --git a/api/src/main/java/com/google/common/eventbus/AnnotatedHandlerFinder.java b/api/src/main/java/com/google/common/eventbus/AnnotatedHandlerFinder.java deleted file mode 100644 index 8ea01451..00000000 --- a/api/src/main/java/com/google/common/eventbus/AnnotatedHandlerFinder.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2007 The Guava Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.common.eventbus; - -import com.google.common.base.Throwables; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Multimap; -import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.UncheckedExecutionException; - -import java.lang.reflect.Method; -import java.util.Set; - -/** - * A {@link HandlerFindingStrategy} for collecting all event handler methods that are marked with - * the {@link Subscribe} annotation. - * - * @author Cliff Biffle - * @author Louis Wasserman - */ -class AnnotatedHandlerFinder implements HandlerFindingStrategy { - /** - * A thread-safe cache that contains the mapping from each class to all methods in that class and - * all super-classes, that are annotated with {@code @Subscribe}. The cache is shared across all - * instances of this class; this greatly improves performance if multiple EventBus instances are - * created and objects of the same class are registered on all of them. - */ - private static final LoadingCache, ImmutableList> handlerMethodsCache = - CacheBuilder.newBuilder() - .weakKeys() - .build(new CacheLoader, ImmutableList>() { - @Override - public ImmutableList load(Class concreteClass) throws Exception { - return getAnnotatedMethodsInternal(concreteClass); - } - }); - - /** - * {@inheritDoc} - * - * This implementation finds all methods marked with a {@link Subscribe} annotation. - */ - @Override - public Multimap, EventHandler> findAllHandlers(Object listener) { - Multimap, EventHandler> methodsInListener = HashMultimap.create(); - Class clazz = listener.getClass(); - for (Method method : getAnnotatedMethods(clazz)) { - Class[] parameterTypes = method.getParameterTypes(); - Class eventType = parameterTypes[0]; - EventHandler handler = new EventHandler(listener, method); - methodsInListener.put(eventType, handler); - } - return methodsInListener; - } - - private static ImmutableList getAnnotatedMethods(Class clazz) { - try { - return handlerMethodsCache.getUnchecked(clazz); - } catch (UncheckedExecutionException e) { - throw Throwables.propagate(e.getCause()); - } - } - - private static ImmutableList getAnnotatedMethodsInternal(Class clazz) { - Set> supers = TypeToken.of(clazz).getTypes().rawTypes(); - ImmutableList.Builder result = ImmutableList.builder(); - for (Method method : clazz.getMethods()) { - /* - * Iterate over each distinct method of {@code clazz}, checking if it is annotated with - * @Subscribe by any of the superclasses or superinterfaces that declare it. - */ - for (Class c : supers) { - try { - Method m = c.getMethod(method.getName(), method.getParameterTypes()); - if (m.isAnnotationPresent(Subscribe.class)) { - 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]; - result.add(method); - break; - } - } catch (NoSuchMethodException ignored) { - // Move on. - } - } - } - return result.build(); - } -} diff --git a/api/src/main/java/com/google/common/eventbus/EventBus.java b/api/src/main/java/com/google/common/eventbus/EventBus.java deleted file mode 100644 index 9360a155..00000000 --- a/api/src/main/java/com/google/common/eventbus/EventBus.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright (C) 2007 The Guava Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.common.eventbus; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.annotations.Beta; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Throwables; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.SetMultimap; -import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.UncheckedExecutionException; - -import java.lang.reflect.InvocationTargetException; -import java.util.Collection; -import java.util.LinkedList; -import java.util.Map.Entry; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Dispatches events to listeners, and provides ways for listeners to register - * themselves. - * - *

The EventBus allows publish-subscribe-style communication between - * components without requiring the components to explicitly register with one - * another (and thus be aware of each other). It is designed exclusively to - * replace traditional Java in-process event distribution using explicit - * registration. It is not a general-purpose publish-subscribe system, - * nor is it intended for interprocess communication. - * - *

Receiving Events

- * To receive events, an object should:
    - *
  1. Expose a public method, known as the event handler, which accepts - * a single argument of the type of event desired;
  2. - *
  3. Mark it with a {@link Subscribe} annotation;
  4. - *
  5. Pass itself to an EventBus instance's {@link #register(Object)} method. - *
  6. - *
- * - *

Posting Events

- * To post an event, simply provide the event object to the - * {@link #post(Object)} method. The EventBus instance will determine the type - * of event and route it to all registered listeners. - * - *

Events are routed based on their type — an event will be delivered - * to any handler for any type to which the event is assignable. This - * includes implemented interfaces, all superclasses, and all interfaces - * implemented by superclasses. - * - *

When {@code post} is called, all registered handlers for an event are run - * in sequence, so handlers should be reasonably quick. If an event may trigger - * an extended process (such as a database load), spawn a thread or queue it for - * later. (For a convenient way to do this, use an {@link AsyncEventBus}.) - * - *

Handler Methods

- * Event handler methods must accept only one argument: the event. - * - *

Handlers should not, in general, throw. If they do, the EventBus will - * catch and log the exception. This is rarely the right solution for error - * handling and should not be relied upon; it is intended solely to help find - * problems during development. - * - *

The EventBus guarantees that it will not call a handler method from - * multiple threads simultaneously, unless the method explicitly allows it by - * bearing the {@link AllowConcurrentEvents} annotation. If this annotation is - * not present, handler methods need not worry about being reentrant, unless - * also called from outside the EventBus. - * - *

Dead Events

- * If an event is posted, but no registered handlers can accept it, it is - * considered "dead." To give the system a second chance to handle dead events, - * they are wrapped in an instance of {@link DeadEvent} and reposted. - * - *

If a handler for a supertype of all events (such as Object) is registered, - * no event will ever be considered dead, and no DeadEvents will be generated. - * Accordingly, while DeadEvent extends {@link Object}, a handler registered to - * receive any Object will never receive a DeadEvent. - * - *

This class is safe for concurrent use. - * - *

See the Guava User Guide article on - * {@code EventBus}. - * - * @author Cliff Biffle - * @since 10.0 - */ -@Beta -public class EventBus { - - /** - * A thread-safe cache for flattenHierarchy(). The Class class is immutable. This cache is shared - * across all EventBus instances, which greatly improves performance if multiple such instances - * are created and objects of the same class are posted on all of them. - */ - private static final LoadingCache, Set>> flattenHierarchyCache = - CacheBuilder.newBuilder() - .weakKeys() - .build(new CacheLoader, Set>>() { - @SuppressWarnings({"unchecked", "rawtypes"}) // safe cast - @Override - public Set> load(Class concreteClass) { - return (Set) TypeToken.of(concreteClass).getTypes().rawTypes(); - } - }); - - /** - * All registered event handlers, indexed by event type. - * - *

This SetMultimap is NOT safe for concurrent use; all access should be - * made after acquiring a read or write lock via {@link #handlersByTypeLock}. - */ - private final SetMultimap, EventHandler> handlersByType = - HashMultimap.create(); - private final ReadWriteLock handlersByTypeLock = new ReentrantReadWriteLock(); - - /** - * Logger for event dispatch failures. Named by the fully-qualified name of - * this class, followed by the identifier provided at construction. - */ - private final Logger logger; - - /** - * Strategy for finding handler methods in registered objects. Currently, - * only the {@link AnnotatedHandlerFinder} is supported, but this is - * encapsulated for future expansion. - */ - private final HandlerFindingStrategy finder = new AnnotatedHandlerFinder(); - - /** queues of events for the current thread to dispatch */ - private final ThreadLocal> eventsToDispatch = - new ThreadLocal>() { - @Override protected Queue initialValue() { - return new LinkedList(); - } - }; - - /** - * Creates a new EventBus named "default". - */ - public EventBus() { - this("default"); - } - - /** - * Creates a new EventBus with the given {@code identifier}. - * - * @param identifier a brief name for this bus, for logging purposes. Should - * be a valid Java identifier. - */ - public EventBus(String identifier) { - logger = Logger.getLogger(EventBus.class.getName() + "." + checkNotNull(identifier)); - } - - /** - * Registers all handler methods on {@code object} to receive events. - * Handler methods are selected and classified using this EventBus's - * {@link HandlerFindingStrategy}; the default strategy is the - * {@link AnnotatedHandlerFinder}. - * - * @param object object whose handler methods should be registered. - */ - public void register(Object object) { - Multimap, EventHandler> methodsInListener = - finder.findAllHandlers(object); - handlersByTypeLock.writeLock().lock(); - try { - handlersByType.putAll(methodsInListener); - } finally { - handlersByTypeLock.writeLock().unlock(); - } - } - - /** - * 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) { - Multimap, EventHandler> methodsInListener = finder.findAllHandlers(object); - for (Entry, Collection> entry : methodsInListener.asMap().entrySet()) { - Class eventType = entry.getKey(); - Collection eventMethodsInListener = entry.getValue(); - - handlersByTypeLock.writeLock().lock(); - try { - Set currentHandlers = handlersByType.get(eventType); - if (!currentHandlers.containsAll(eventMethodsInListener)) { - throw new IllegalArgumentException( - "missing event handler for an annotated method. Is " + object + " registered?"); - } - currentHandlers.removeAll(eventMethodsInListener); - } finally { - handlersByTypeLock.writeLock().unlock(); - } - } - } - - /** - * 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) { - Set> dispatchTypes = flattenHierarchy(event.getClass()); - - boolean dispatched = false; - for (Class eventType : dispatchTypes) { - handlersByTypeLock.readLock().lock(); - try { - Set wrappers = handlersByType.get(eventType); - - if (!wrappers.isEmpty()) { - dispatched = true; - for (EventHandler wrapper : wrappers) { - enqueueEvent(event, wrapper); - } - } - } finally { - handlersByTypeLock.readLock().unlock(); - } - } - - if (!dispatched && !(event instanceof DeadEvent)) { - post(new DeadEvent(this, event)); - } - - dispatchQueuedEvents(); - } - - /** - * Queue the {@code event} for dispatch during - * {@link #dispatchQueuedEvents()}. Events are queued in-order of occurrence - * so they can be dispatched in the same order. - */ - void enqueueEvent(Object event, EventHandler handler) { - eventsToDispatch.get().offer(new EventWithHandler(event, handler)); - } - - /** - * Drain the queue of events to be dispatched. As the queue is being drained, - * new events may be posted to the end of the queue. - */ - void dispatchQueuedEvents() { - try { - Queue events = eventsToDispatch.get(); - EventWithHandler eventWithHandler; - while ((eventWithHandler = events.poll()) != null) { - dispatch(eventWithHandler.event, eventWithHandler.handler); - } - } finally { - eventsToDispatch.remove(); - } - } - - /** - * Dispatches {@code event} to the handler in {@code wrapper}. This method - * is an appropriate override point for subclasses that wish to make - * event delivery asynchronous. - * - * @param event event to dispatch. - * @param wrapper wrapper that will call the handler. - */ - void dispatch(Object event, EventHandler wrapper) { - try { - wrapper.handleEvent(event); - } catch (InvocationTargetException e) { - logger.log(Level.SEVERE, - "Could not dispatch event: " + event + " to handler " + wrapper, e); - } - } - - /** - * 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. - */ - @VisibleForTesting - Set> flattenHierarchy(Class concreteClass) { - try { - return flattenHierarchyCache.getUnchecked(concreteClass); - } catch (UncheckedExecutionException e) { - throw Throwables.propagate(e.getCause()); - } - } - - /** simple struct representing an event and it's handler */ - static class EventWithHandler { - final Object event; - final EventHandler handler; - public EventWithHandler(Object event, EventHandler handler) { - this.event = checkNotNull(event); - this.handler = checkNotNull(handler); - } - } -} diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java index de5a1b81..06d282e6 100644 --- a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java +++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java @@ -1,10 +1,10 @@ package net.md_5.bungee.api.plugin; import com.google.common.base.Preconditions; -import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import java.io.File; import java.io.InputStream; +import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; @@ -20,7 +20,8 @@ import lombok.RequiredArgsConstructor; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.ProxyServer; -import net.md_5.bungee.api.event.LoginEvent; +import net.md_5.bungee.event.EventBus; +import net.md_5.bungee.event.EventHandler; import org.yaml.snakeyaml.Yaml; /** @@ -36,7 +37,8 @@ public class PluginManager private final ProxyServer proxy; /*========================================================================*/ private final Yaml yaml = new Yaml(); - private final EventBus eventBus = new EventBus(); + @SuppressWarnings("unchecked") + private final EventBus eventBus = new EventBus( ProxyServer.getInstance().getLogger(), Subscribe.class, EventHandler.class ); private final Map plugins = new HashMap<>(); private final Map commandMap = new HashMap<>(); @@ -311,6 +313,14 @@ public class PluginManager */ public void registerListener(Plugin plugin, Listener listener) { + for ( Method method : listener.getClass().getDeclaredMethods() ) + { + if ( method.isAnnotationPresent( Subscribe.class ) ) + { + proxy.getLogger().log( Level.SEVERE, "Listener {0} has registered using depreceated subscribe annotation! Please advice author to update to @EventHadler", listener ); + } + } + eventBus.register( listener ); } } diff --git a/event/pom.xml b/event/pom.xml new file mode 100644 index 00000000..a21bbe23 --- /dev/null +++ b/event/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + net.md-5 + bungeecord-parent + 1.5-SNAPSHOT + ../pom.xml + + + net.md-5 + bungeecord-event + 1.5-SNAPSHOT + jar + + BungeeCord-Event + Generic java event dispatching API intended for use with BungeeCord + + + + junit + junit + 4.11 + test + + + diff --git a/event/src/main/java/net/md_5/bungee/event/EventBus.java b/event/src/main/java/net/md_5/bungee/event/EventBus.java new file mode 100644 index 00000000..94e154ea --- /dev/null +++ b/event/src/main/java/net/md_5/bungee/event/EventBus.java @@ -0,0 +1,163 @@ +package net.md_5.bungee.event; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class EventBus +{ + + private final Map, Map> eventToHandler = new HashMap<>(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final Logger logger; + private final Class[] annotations; + + public EventBus() + { + this( null, (Class[]) null ); + } + + public EventBus(Logger logger) + { + this( logger, (Class[]) null ); + } + + @SuppressWarnings("unchecked") + public EventBus(Class... annotations) + { + this( null, annotations ); + } + + @SuppressWarnings("unchecked") + public EventBus(Logger logger, Class... annotations) + { + this.logger = ( logger == null ) ? Logger.getGlobal() : logger; + this.annotations = ( annotations == null || annotations.length == 0 ) ? new Class[] + { + EventHandler.class + } : annotations; + } + + public void post(Object event) + { + lock.readLock().lock(); + try + { + Map handlers = eventToHandler.get( event.getClass() ); + if ( handlers != null ) + { + for ( Map.Entry handler : handlers.entrySet() ) + { + for ( Method method : handler.getValue() ) + { + try + { + method.invoke( handler.getKey(), event ); + } catch ( IllegalAccessException ex ) + { + throw new Error( "Method became inaccessible: " + event, ex ); + } catch ( IllegalArgumentException ex ) + { + throw new Error( "Method rejected target/argument: " + event, ex ); + } catch ( InvocationTargetException ex ) + { + logger.log( Level.WARNING, MessageFormat.format( "Error dispatching event {0} to listener {1}", event, handler.getKey() ), ex.getCause() ); + } + } + } + } + } finally + { + lock.readLock().unlock(); + } + } + + private Map, Set> findHandlers(Object listener) + { + Map, Set> handler = new HashMap<>(); + for ( Method m : listener.getClass().getDeclaredMethods() ) + { + for ( Class annotation : annotations ) + { + if ( m.isAnnotationPresent( annotation ) ) + { + Class[] params = m.getParameterTypes(); + if ( params.length != 1 ) + { + logger.log( Level.INFO, "Method {0} in class {1} annotated with {2} does not have single argument", new Object[] + { + m, listener.getClass(), annotation + } ); + continue; + } + + Set existing = handler.get( params[0] ); + if ( existing == null ) + { + existing = new HashSet<>(); + handler.put( params[0], existing ); + } + existing.add( m ); + break; + } + } + } + return handler; + } + + public void register(Object listener) + { + Map, Set> handler = findHandlers( listener ); + lock.writeLock().lock(); + try + { + for ( Map.Entry, Set> e : handler.entrySet() ) + { + Map a = eventToHandler.get( e.getKey() ); + if ( a == null ) + { + a = new HashMap<>(); + eventToHandler.put( e.getKey(), a ); + } + Method[] baked = new Method[ e.getValue().size() ]; + a.put( listener, e.getValue().toArray( baked ) ); + } + } finally + { + lock.writeLock().unlock(); + } + } + + public void unregister(Object listener) + { + Map, Set> handler = findHandlers( listener ); + lock.writeLock().lock(); + try + { + for ( Map.Entry, Set> e : handler.entrySet() ) + { + Map a = eventToHandler.get( e.getKey() ); + if ( a != null ) + { + a.remove( listener ); + if ( a.isEmpty() ) + { + eventToHandler.remove( e.getKey() ); + } + } + } + } finally + { + lock.writeLock().unlock(); + } + } +} diff --git a/event/src/main/java/net/md_5/bungee/event/EventHandler.java b/event/src/main/java/net/md_5/bungee/event/EventHandler.java new file mode 100644 index 00000000..74a79264 --- /dev/null +++ b/event/src/main/java/net/md_5/bungee/event/EventHandler.java @@ -0,0 +1,13 @@ +package net.md_5.bungee.event; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface EventHandler +{ +} diff --git a/api/src/test/java/net/md_5/bungee/api/plugin/EventBusTest.java b/event/src/test/java/net/md_5/bungee/event/EventBusTest.java similarity index 64% rename from api/src/test/java/net/md_5/bungee/api/plugin/EventBusTest.java rename to event/src/test/java/net/md_5/bungee/event/EventBusTest.java index e7d1705c..4a6f0934 100644 --- a/api/src/test/java/net/md_5/bungee/api/plugin/EventBusTest.java +++ b/event/src/test/java/net/md_5/bungee/event/EventBusTest.java @@ -1,7 +1,5 @@ -package net.md_5.bungee.api.plugin; +package net.md_5.bungee.event; -import com.google.common.eventbus.EventBus; -import com.google.common.eventbus.Subscribe; import java.util.concurrent.CountDownLatch; import org.junit.Assert; import org.junit.Test; @@ -10,33 +8,35 @@ public class EventBusTest { private final EventBus bus = new EventBus(); - private final CountDownLatch latch = new CountDownLatch( 1 ); + private final CountDownLatch latch = new CountDownLatch( 2 ); @Test public void testNestedEvents() { bus.register( this ); bus.post( new FirstEvent() ); - } - - @Subscribe - public void firstListener(FirstEvent event) - { - bus.post( new SecondEvent() ); Assert.assertEquals( latch.getCount(), 0 ); } - @Subscribe + @EventHandler + public void firstListener(FirstEvent event) + { + bus.post( new SecondEvent() ); + Assert.assertEquals( latch.getCount(), 1 ); + latch.countDown(); + } + + @EventHandler public void secondListener(SecondEvent event) { latch.countDown(); } - public static class FirstEvent extends Event + public static class FirstEvent { } - public static class SecondEvent extends Event + public static class SecondEvent { } } diff --git a/pom.xml b/pom.xml index 1ba8d95d..1623896c 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ api + event protocol proxy