package org.limewire.listener; import java.awt.EventQueue; import java.lang.reflect.Method; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import org.limewire.concurrent.ExecutorsHelper; import org.limewire.concurrent.ThreadExecutor; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.util.ExceptionUtils; import org.limewire.util.Objects; /** * Maintains event listeners and broadcasts events to all listeners. * <p> * The annotations {@link SwingEDTEvent} and {@link BlockingEvent} can be added * to implementations of {@link EventListener#handleEvent(Object)} in order to * allow those events to be dispatched on the EDT thread or a new thread. * <p> * If classes want to delegate implementations of {@link EventListener}, it is * important that the delegate listener's <code>handleEvent(E)</code> method * is called via {@link EventListenerList#dispatch(EventListener, Object, EventListenerListContext)}. This * ensures that the event is dispatched appropriately, according to the * annotation on the delegate listener. */ public class EventListenerList<E> implements ListenerSupport<E>, EventBroadcaster<E> { private final List<ListenerProxy<E>> listenerList = new CopyOnWriteArrayList<ListenerProxy<E>>(); private final Log log; private final EventListenerListContext context; /** Constructs an {@link EventListenerList} with a new context and no log. */ public EventListenerList() { this(null, new EventListenerListContext()); } /** Constructs an {@link EventListenerList} with a new context a log based on the given class. */ public EventListenerList(Class loggerKey) { this(LogFactory.getLog(loggerKey), new EventListenerListContext()); } /** Constructs an {@link EventListenerList} with a new context the given log. */ public EventListenerList(Log log) { this(log, new EventListenerListContext()); } /** Constructs an {@link EventListenerList} with the given context and no log. */ public EventListenerList(EventListenerListContext context) { this(null, context); } /** Constructs an {@link EventListenerList} with the given context and log. */ public EventListenerList(Log log, EventListenerListContext context) { this.log = log; this.context = context; } /** * Dispatches the event to the listener. This scans the listener for * annotations and dispatches in the correct thread, according to the * annotation. * * @param context the {@link EventListenerListContext} context to dispatch the * event with. Usually retrieved by * {@link EventListenerList#getContext()}. The context may be null, * which will result in context not being used. */ public static <E> void dispatch(EventListener<E> listener, E event, EventListenerListContext context) { EventListener<E> proxy = new ListenerProxy<E>(null, Objects.nonNull(listener, "listener"), context); proxy.handleEvent(event); } /** * Returns the context which can be used to dispatch events via * {@link EventListenerList#dispatch(EventListener, Object, org.limewire.listener.EventListenerList.EventListenerListContext)}. */ public EventListenerListContext getContext() { return context; } /** Adds the listener. */ public void addListener(EventListener<E> listener) { if(log != null) { log.debugf("adding listener {0} to {1}", listener, this); } listenerList.add(new ListenerProxy<E>(log, Objects.nonNull(listener, "listener"), context)); } /** Returns true if the listener was removed. */ public boolean removeListener(EventListener<E> listener) { if(log != null) { log.debugf("removing listener {0} from {1}", listener, this); } Objects.nonNull(listener, "listener"); // Find the proxy, then remove it. for(ListenerProxy<E> proxyListener : listenerList) { if(proxyListener.delegate.equals(listener)) { return listenerList.remove(proxyListener); } } return false; } /** * Notifies just the given listener about the given event. * This uses the {@link EventListenerListContext} of the current {@link EventListenerList}. * If you need to use another context, use {@link EventListenerList#dispatch(EventListener, Object, EventListenerListContext)}. */ public void dispatch(EventListener<E> listener, E event) { EventListenerList.dispatch(listener, event, context); } /** Broadcasts an event to all listeners. */ public void broadcast(E event) { Objects.nonNull(event, "event"); if(log != null) { log.debugf("broadcasting event {0} from {1}", event, this); } // When broadcasting, capture exceptions to make sure each listeners // gets a shot. If an exception occurs and can be reported immediately, // it is reported. Otherwise, is captured & the first exception // is reported after the loop finishes. Throwable t = null; for(ListenerProxy<E> listener : listenerList) { try { listener.handleEvent(event); } catch(Throwable thrown) { if(log != null) { log.error("error dispatching " + event, thrown); } thrown = ExceptionUtils.reportOrReturn(thrown); if(thrown != null && t == null) { t = thrown; } } } if(t != null) { ExceptionUtils.reportOrRethrow(t); } } /** Returns the size of the list. */ public int size() { return listenerList.size(); } private static final class ListenerProxy<E> implements EventListener<E> { private final Log log; private final EventListener<E> delegate; private final EventListenerListContext context; private volatile DispatchStrategy strategy = DispatchStrategy.UNKNOWN; public ListenerProxy(Log log, EventListener<E> delegate, EventListenerListContext context) { this.log = log; this.delegate = delegate; this.context = context; } @Override public void handleEvent(final E event) { // Note: This is not thread-safe, but it is OK to analyze multiple times. // The internals of analyze make sure that only one ExecutorMap & Executor are // ever set. if(strategy == DispatchStrategy.UNKNOWN) { strategy = analyze(delegate, event); } if(log != null) { log.tracef("Dispatching event {0} to {1} with strategy {2}", event, delegate, strategy); } strategy.dispatch(delegate, event); } /** * Loops through all 'handleEvent' methods whose parameter type can match event's * classes & superclasses. When one is found, see if it is annotated with {@link SwingEDTEvent} or * {@link BlockingEvent}. */ private DispatchStrategy analyze(EventListener<E> delegate, E event) { Class<?> eventClass = event.getClass(); Method method = null; while(eventClass != null) { try { method = delegate.getClass().getMethod("handleEvent", eventClass); break; } catch (NoSuchMethodException ignored) { } eventClass = eventClass.getSuperclass(); } if(method == null) { throw new IllegalStateException("Unable to find method!"); } BlockingEvent blockingEvent; if(method.getAnnotation(SwingEDTEvent.class) != null) { return DispatchStrategy.SWING; } else if((blockingEvent = method.getAnnotation(BlockingEvent.class)) != null) { return DispatchStrategy.getBlockingStrategy(context, blockingEvent); } else { return DispatchStrategy.INLINE; } } } /** A strategy to dispatch events. */ private static abstract class DispatchStrategy { /** Dispatches the event, possibly using the given executor. */ abstract <E> void dispatch(EventListener<E> listener, E event); /** A strategy that always fails. */ public static DispatchStrategy UNKNOWN = new DispatchStrategy() { @Override <E> void dispatch(EventListener<E> listener, E event) { throw new IllegalStateException("unknown dispatch!"); } }; /** A strategy that runs immediately. */ public static DispatchStrategy INLINE = new DispatchStrategy() { @Override <E> void dispatch(EventListener<E> listener, E event) { listener.handleEvent(event); } }; /** A strategy that dispatches on the Swing thread. */ public static DispatchStrategy SWING = new DispatchStrategy() { @Override <E> void dispatch(final EventListener<E> listener, final E event) { if(EventQueue.isDispatchThread()) { listener.handleEvent(event); } else { EventQueue.invokeLater(new Runnable() { public void run() { listener.handleEvent(event); } }); } } }; /** A strategy that creates a new thread. */ public static DispatchStrategy NEW_THREAD = new DispatchStrategy() { @Override <E> void dispatch(final EventListener<E> listener, final E event) { Runnable runner = new Runnable() { public void run() { listener.handleEvent(event); } }; ThreadExecutor.startThread(runner, "BlockingEvent"); } }; /** Returns the appropriate dispatch strategy for this event. */ public static DispatchStrategy getBlockingStrategy(EventListenerListContext context, BlockingEvent event) { if(context == null) { return NEW_THREAD; } else { Executor executor = context.getOrCreateExecutor(event.queueName()); if(executor == null) { return NEW_THREAD; } else { return new ExecutorDispatchStrategy(executor); } } }; } /** A strategy that runs in the given executor. */ private static class ExecutorDispatchStrategy extends DispatchStrategy { private final Executor executor; public ExecutorDispatchStrategy(Executor executor) { this.executor = executor; } @Override <E> void dispatch(final org.limewire.listener.EventListener<E> listener, final E event) { Runnable runner = new Runnable() { public void run() { listener.handleEvent(event); } }; executor.execute(runner); } } /** * The context of an {@link EventListenerList}. The context is used to ensure * that any state required over multiple event notifications within a single list * is maintained, such as notifying {@link BlockingEvent BlockingEvents} with a specific * {@link BlockingEvent#queueName()} in the same queue. */ public static final class EventListenerListContext { // The use of AtomicReference here is to allow for cost-free instantiation // of a context. Most contexts will not have any named executors, // so it would be wasteful to create a map to store them for every // EventListenerList. In the event that a named executor is required, // the map will be created on-demand. private final AtomicReference<ConcurrentMap<String, Executor>> eventExecutorsRef = new AtomicReference<ConcurrentMap<String,Executor>>(); private Executor getOrCreateExecutor(String queueName) { if(queueName != null && !queueName.equals("")) { ConcurrentMap<String, Executor> executorMap = eventExecutorsRef.get(); // If no executorMap exists already, create a new one. if(executorMap == null) { eventExecutorsRef.compareAndSet(null, new ConcurrentHashMap<String, Executor>()); executorMap = eventExecutorsRef.get(); } // If no executor exists for this queueName, create a new one. Executor executor = executorMap.get(queueName); if(executor == null) { executorMap.putIfAbsent(queueName, ExecutorsHelper.newProcessingQueue("BlockingEventQueue-" + queueName)); executor = executorMap.get(queueName); } return executor; } else { return null; // No executor required. } } } }