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