// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.tools;
import java.lang.ref.WeakReference;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Stream;
import org.openstreetmap.josm.Main;
/**
* This is a list of listeners. It does error checking and allows you to fire all listeners.
*
* @author Michael Zangl
* @param <T> The type of listener contained in this list.
* @since 10824
*/
public class ListenerList<T> {
/**
* This is a function that can be invoked for every listener.
* @param <T> the listener type.
*/
@FunctionalInterface
public interface EventFirerer<T> {
/**
* Should fire the event for the given listener.
* @param listener The listener to fire the event for.
*/
void fire(T listener);
}
private static final class WeakListener<T> {
private final WeakReference<T> listener;
WeakListener(T listener) {
this.listener = new WeakReference<>(listener);
}
@Override
public boolean equals(Object obj) {
if (obj != null && obj.getClass() == WeakListener.class) {
return Objects.equals(listener.get(), ((WeakListener<?>) obj).listener.get());
} else {
return false;
}
}
@Override
public int hashCode() {
T l = listener.get();
if (l == null) {
return 0;
} else {
return l.hashCode();
}
}
@Override
public String toString() {
return "WeakListener [listener=" + listener + ']';
}
}
private final CopyOnWriteArrayList<T> listeners = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<WeakListener<T>> weakListeners = new CopyOnWriteArrayList<>();
protected ListenerList() {
// hide
}
/**
* Adds a listener. The listener will not prevent the object from being garbage collected.
*
* This should be used with care. It is better to add good cleanup code.
* @param listener The listener.
*/
public synchronized void addWeakListener(T listener) {
if (ensureNotInList(listener)) {
// clean the weak listeners, just to be sure...
while (weakListeners.remove(new WeakListener<T>(null))) {
// continue
}
weakListeners.add(new WeakListener<>(listener));
}
}
/**
* Adds a listener.
* @param listener The listener to add.
*/
public synchronized void addListener(T listener) {
if (ensureNotInList(listener)) {
listeners.add(listener);
}
}
private boolean ensureNotInList(T listener) {
CheckParameterUtil.ensureParameterNotNull(listener, "listener");
if (containsListener(listener)) {
failAdd(listener);
return false;
} else {
return true;
}
}
protected void failAdd(T listener) {
throw new IllegalArgumentException(
MessageFormat.format("Listener {0} (instance of {1}) was already registered.", listener,
listener.getClass().getName()));
}
private boolean containsListener(T listener) {
return listeners.contains(listener) || weakListeners.contains(new WeakListener<>(listener));
}
/**
* Removes a listener.
* @param listener The listener to remove.
* @throws IllegalArgumentException if the listener was not registered before
*/
public synchronized void removeListener(T listener) {
if (!listeners.remove(listener) && !weakListeners.remove(new WeakListener<>(listener))) {
failRemove(listener);
}
}
protected void failRemove(T listener) {
throw new IllegalArgumentException(
MessageFormat.format("Listener {0} (instance of {1}) was not registered before or already removed.",
listener, listener.getClass().getName()));
}
/**
* Check if any listeners are registered.
* @return <code>true</code> if any are registered.
*/
public boolean hasListeners() {
return !listeners.isEmpty();
}
/**
* Fires an event to every listener.
* @param eventFirerer The firerer to invoke the event method of the listener.
*/
public void fireEvent(EventFirerer<T> eventFirerer) {
for (T l : listeners) {
eventFirerer.fire(l);
}
for (Iterator<WeakListener<T>> iterator = weakListeners.iterator(); iterator.hasNext();) {
WeakListener<T> weakLink = iterator.next();
T l = weakLink.listener.get();
if (l != null) {
// cleanup during add() should be enough to not cause memory leaks
// therefore, we ignore null listeners.
eventFirerer.fire(l);
}
}
}
/**
* This is a special {@link ListenerList} that traces calls to the add/remove methods. This may cause memory leaks.
* @author Michael Zangl
*
* @param <T> The type of listener contained in this list
*/
public static class TracingListenerList<T> extends ListenerList<T> {
private final HashMap<T, StackTraceElement[]> listenersAdded = new HashMap<>();
private final HashMap<T, StackTraceElement[]> listenersRemoved = new HashMap<>();
protected TracingListenerList() {
// hidden
}
@Override
public synchronized void addListener(T listener) {
super.addListener(listener);
listenersRemoved.remove(listener);
listenersAdded.put(listener, Thread.currentThread().getStackTrace());
}
@Override
public synchronized void addWeakListener(T listener) {
super.addWeakListener(listener);
listenersRemoved.remove(listener);
listenersAdded.put(listener, Thread.currentThread().getStackTrace());
}
@Override
public synchronized void removeListener(T listener) {
super.removeListener(listener);
listenersAdded.remove(listener);
listenersRemoved.put(listener, Thread.currentThread().getStackTrace());
}
@Override
protected void failAdd(T listener) {
Main.trace("Previous addition of the listener");
dumpStack(listenersAdded.get(listener));
super.failAdd(listener);
}
@Override
protected void failRemove(T listener) {
Main.trace("Previous removal of the listener");
dumpStack(listenersRemoved.get(listener));
super.failRemove(listener);
}
private static void dumpStack(StackTraceElement ... stackTraceElements) {
if (stackTraceElements == null) {
Main.trace(" - (no trace recorded)");
} else {
Stream.of(stackTraceElements).limit(20).forEach(
e -> Main.trace(e.getClassName() + "." + e.getMethodName() + " line " + e.getLineNumber()));
}
}
}
private static class UncheckedListenerList<T> extends ListenerList<T> {
@Override
protected void failAdd(T listener) {
Logging.warn("Listener was alreaady added: {0}", listener);
// ignore
}
@Override
protected void failRemove(T listener) {
Logging.warn("Listener was removed twice or not added: {0}", listener);
// ignore
}
}
/**
* Create a new listener list
* @param <T> The listener type the list should hold.
* @return A new list. A tracing list is created if trace is enabled.
*/
public static <T> ListenerList<T> create() {
if (Main.isTraceEnabled()) {
return new TracingListenerList<>();
} else {
return new ListenerList<>();
}
}
/**
* Creates a new listener list that does not fail if listeners are added ore removed twice.
* <p>
* Use of this list is discouraged. You should always use {@link #create()} in new implementations and check your listeners.
* @param <T> The listener type
* @return A new list.
* @since 11224
*/
public static <T> ListenerList<T> createUnchecked() {
return new UncheckedListenerList<>();
}
}