001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.lang.ref.WeakReference; 005import java.text.MessageFormat; 006import java.util.HashMap; 007import java.util.Objects; 008import java.util.concurrent.CopyOnWriteArrayList; 009import java.util.stream.Stream; 010 011/** 012 * This is a list of listeners. It does error checking and allows you to fire all listeners. 013 * 014 * @author Michael Zangl 015 * @param <T> The type of listener contained in this list. 016 * @since 10824 017 */ 018public class ListenerList<T> { 019 /** 020 * This is a function that can be invoked for every listener. 021 * @param <T> the listener type. 022 */ 023 @FunctionalInterface 024 public interface EventFirerer<T> { 025 /** 026 * Should fire the event for the given listener. 027 * @param listener The listener to fire the event for. 028 */ 029 void fire(T listener); 030 } 031 032 private static final class WeakListener<T> { 033 034 private final WeakReference<T> listener; 035 036 WeakListener(T listener) { 037 this.listener = new WeakReference<>(listener); 038 } 039 040 @Override 041 public boolean equals(Object obj) { 042 if (obj != null && obj.getClass() == WeakListener.class) { 043 return Objects.equals(listener.get(), ((WeakListener<?>) obj).listener.get()); 044 } else { 045 return false; 046 } 047 } 048 049 @Override 050 public int hashCode() { 051 T l = listener.get(); 052 if (l == null) { 053 return 0; 054 } else { 055 return l.hashCode(); 056 } 057 } 058 059 @Override 060 public String toString() { 061 return "WeakListener [listener=" + listener + ']'; 062 } 063 } 064 065 private final CopyOnWriteArrayList<T> listeners = new CopyOnWriteArrayList<>(); 066 private final CopyOnWriteArrayList<WeakListener<T>> weakListeners = new CopyOnWriteArrayList<>(); 067 068 protected ListenerList() { 069 // hide 070 } 071 072 /** 073 * Adds a listener. The listener will not prevent the object from being garbage collected. 074 * 075 * This should be used with care. It is better to add good cleanup code. 076 * @param listener The listener. 077 */ 078 public synchronized void addWeakListener(T listener) { 079 if (ensureNotInList(listener)) { 080 // clean the weak listeners, just to be sure... 081 while (weakListeners.remove(new WeakListener<T>(null))) { 082 // continue 083 } 084 weakListeners.add(new WeakListener<>(listener)); 085 } 086 } 087 088 /** 089 * Adds a listener. 090 * @param listener The listener to add. 091 */ 092 public synchronized void addListener(T listener) { 093 if (ensureNotInList(listener)) { 094 listeners.add(listener); 095 } 096 } 097 098 private boolean ensureNotInList(T listener) { 099 CheckParameterUtil.ensureParameterNotNull(listener, "listener"); 100 if (containsListener(listener)) { 101 failAdd(listener); 102 return false; 103 } else { 104 return true; 105 } 106 } 107 108 protected void failAdd(T listener) { 109 throw new IllegalArgumentException( 110 MessageFormat.format("Listener {0} (instance of {1}) was already registered.", listener, 111 listener.getClass().getName())); 112 } 113 114 /** 115 * Determines if this listener list contains the given listener. 116 * @param listener listener to find 117 * @return {@code true} is the listener is known 118 * @since 15649 119 */ 120 public synchronized boolean containsListener(T listener) { 121 return listeners.contains(listener) || weakListeners.contains(new WeakListener<>(listener)); 122 } 123 124 /** 125 * Removes a listener. 126 * @param listener The listener to remove. 127 * @throws IllegalArgumentException if the listener was not registered before 128 */ 129 public synchronized void removeListener(T listener) { 130 if (!listeners.remove(listener) && !weakListeners.remove(new WeakListener<>(listener))) { 131 failRemove(listener); 132 } 133 } 134 135 protected void failRemove(T listener) { 136 throw new IllegalArgumentException( 137 MessageFormat.format("Listener {0} (instance of {1}) was not registered before or already removed.", 138 listener, listener.getClass().getName())); 139 } 140 141 /** 142 * Check if any listeners are registered. 143 * @return <code>true</code> if any are registered. 144 */ 145 public boolean hasListeners() { 146 return !listeners.isEmpty(); 147 } 148 149 /** 150 * Fires an event to every listener. 151 * @param eventFirerer The firerer to invoke the event method of the listener. 152 */ 153 public void fireEvent(EventFirerer<T> eventFirerer) { 154 for (T l : listeners) { 155 eventFirerer.fire(l); 156 } 157 for (WeakListener<T> weakLink : weakListeners) { 158 T l = weakLink.listener.get(); 159 if (l != null) { 160 // cleanup during add() should be enough to not cause memory leaks 161 // therefore, we ignore null listeners. 162 eventFirerer.fire(l); 163 } 164 } 165 } 166 167 /** 168 * This is a special {@link ListenerList} that traces calls to the add/remove methods. This may cause memory leaks. 169 * @author Michael Zangl 170 * 171 * @param <T> The type of listener contained in this list 172 */ 173 public static class TracingListenerList<T> extends ListenerList<T> { 174 private final HashMap<T, StackTraceElement[]> listenersAdded = new HashMap<>(); 175 private final HashMap<T, StackTraceElement[]> listenersRemoved = new HashMap<>(); 176 177 protected TracingListenerList() { 178 // hidden 179 } 180 181 @Override 182 public synchronized void addListener(T listener) { 183 super.addListener(listener); 184 listenersRemoved.remove(listener); 185 listenersAdded.put(listener, Thread.currentThread().getStackTrace()); 186 } 187 188 @Override 189 public synchronized void addWeakListener(T listener) { 190 super.addWeakListener(listener); 191 listenersRemoved.remove(listener); 192 listenersAdded.put(listener, Thread.currentThread().getStackTrace()); 193 } 194 195 @Override 196 public synchronized void removeListener(T listener) { 197 super.removeListener(listener); 198 listenersAdded.remove(listener); 199 listenersRemoved.put(listener, Thread.currentThread().getStackTrace()); 200 } 201 202 @Override 203 protected void failAdd(T listener) { 204 Logging.trace("Previous addition of the listener"); 205 dumpStack(listenersAdded.get(listener)); 206 super.failAdd(listener); 207 } 208 209 @Override 210 protected void failRemove(T listener) { 211 Logging.trace("Previous removal of the listener"); 212 dumpStack(listenersRemoved.get(listener)); 213 super.failRemove(listener); 214 } 215 216 private static void dumpStack(StackTraceElement... stackTraceElements) { 217 if (stackTraceElements == null) { 218 Logging.trace(" - (no trace recorded)"); 219 } else { 220 Stream.of(stackTraceElements).limit(20).forEach( 221 e -> Logging.trace(e.getClassName() + "." + e.getMethodName() + " line " + e.getLineNumber())); 222 } 223 } 224 } 225 226 private static class UncheckedListenerList<T> extends ListenerList<T> { 227 @Override 228 protected void failAdd(T listener) { 229 Logging.warn("Listener was already added: {0}", listener); 230 // ignore 231 } 232 233 @Override 234 protected void failRemove(T listener) { 235 Logging.warn("Listener was removed twice or not added: {0}", listener); 236 // ignore 237 } 238 } 239 240 /** 241 * Create a new listener list 242 * @param <T> The listener type the list should hold. 243 * @return A new list. A tracing list is created if trace is enabled. 244 */ 245 public static <T> ListenerList<T> create() { 246 if (Logging.isTraceEnabled()) { 247 return new TracingListenerList<>(); 248 } else { 249 return new ListenerList<>(); 250 } 251 } 252 253 /** 254 * Creates a new listener list that does not fail if listeners are added or removed twice. 255 * <p> 256 * Use of this list is discouraged. You should always use {@link #create()} in new implementations and check your listeners. 257 * @param <T> The listener type 258 * @return A new list. 259 * @since 11224 260 */ 261 public static <T> ListenerList<T> createUnchecked() { 262 return new UncheckedListenerList<>(); 263 } 264}