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