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.
}
}
}
}