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 private boolean containsListener(T listener) { 116 return listeners.contains(listener) || weakListeners.contains(new WeakListener<>(listener)); 117 } 118 119 /** 120 * Removes a listener. 121 * @param listener The listener to remove. 122 * @throws IllegalArgumentException if the listener was not registered before 123 */ 124 public synchronized void removeListener(T listener) { 125 if (!listeners.remove(listener) && !weakListeners.remove(new WeakListener<>(listener))) { 126 failRemove(listener); 127 } 128 } 129 130 protected void failRemove(T listener) { 131 throw new IllegalArgumentException( 132 MessageFormat.format("Listener {0} (instance of {1}) was not registered before or already removed.", 133 listener, listener.getClass().getName())); 134 } 135 136 /** 137 * Check if any listeners are registered. 138 * @return <code>true</code> if any are registered. 139 */ 140 public boolean hasListeners() { 141 return !listeners.isEmpty(); 142 } 143 144 /** 145 * Fires an event to every listener. 146 * @param eventFirerer The firerer to invoke the event method of the listener. 147 */ 148 public void fireEvent(EventFirerer<T> eventFirerer) { 149 for (T l : listeners) { 150 eventFirerer.fire(l); 151 } 152 for (Iterator<WeakListener<T>> iterator = weakListeners.iterator(); iterator.hasNext();) { 153 WeakListener<T> weakLink = iterator.next(); 154 T l = weakLink.listener.get(); 155 if (l != null) { 156 // cleanup during add() should be enough to not cause memory leaks 157 // therefore, we ignore null listeners. 158 eventFirerer.fire(l); 159 } 160 } 161 } 162 163 /** 164 * This is a special {@link ListenerList} that traces calls to the add/remove methods. This may cause memory leaks. 165 * @author Michael Zangl 166 * 167 * @param <T> The type of listener contained in this list 168 */ 169 public static class TracingListenerList<T> extends ListenerList<T> { 170 private final HashMap<T, StackTraceElement[]> listenersAdded = new HashMap<>(); 171 private final HashMap<T, StackTraceElement[]> listenersRemoved = new HashMap<>(); 172 173 protected TracingListenerList() { 174 // hidden 175 } 176 177 @Override 178 public synchronized void addListener(T listener) { 179 super.addListener(listener); 180 listenersRemoved.remove(listener); 181 listenersAdded.put(listener, Thread.currentThread().getStackTrace()); 182 } 183 184 @Override 185 public synchronized void addWeakListener(T listener) { 186 super.addWeakListener(listener); 187 listenersRemoved.remove(listener); 188 listenersAdded.put(listener, Thread.currentThread().getStackTrace()); 189 } 190 191 @Override 192 public synchronized void removeListener(T listener) { 193 super.removeListener(listener); 194 listenersAdded.remove(listener); 195 listenersRemoved.put(listener, Thread.currentThread().getStackTrace()); 196 } 197 198 @Override 199 protected void failAdd(T listener) { 200 Logging.trace("Previous addition of the listener"); 201 dumpStack(listenersAdded.get(listener)); 202 super.failAdd(listener); 203 } 204 205 @Override 206 protected void failRemove(T listener) { 207 Logging.trace("Previous removal of the listener"); 208 dumpStack(listenersRemoved.get(listener)); 209 super.failRemove(listener); 210 } 211 212 private static void dumpStack(StackTraceElement... stackTraceElements) { 213 if (stackTraceElements == null) { 214 Logging.trace(" - (no trace recorded)"); 215 } else { 216 Stream.of(stackTraceElements).limit(20).forEach( 217 e -> Logging.trace(e.getClassName() + "." + e.getMethodName() + " line " + e.getLineNumber())); 218 } 219 } 220 } 221 222 private static class UncheckedListenerList<T> extends ListenerList<T> { 223 @Override 224 protected void failAdd(T listener) { 225 Logging.warn("Listener was alreaady added: {0}", listener); 226 // ignore 227 } 228 229 @Override 230 protected void failRemove(T listener) { 231 Logging.warn("Listener was removed twice or not added: {0}", listener); 232 // ignore 233 } 234 } 235 236 /** 237 * Create a new listener list 238 * @param <T> The listener type the list should hold. 239 * @return A new list. A tracing list is created if trace is enabled. 240 */ 241 public static <T> ListenerList<T> create() { 242 if (Logging.isTraceEnabled()) { 243 return new TracingListenerList<>(); 244 } else { 245 return new ListenerList<>(); 246 } 247 } 248 249 /** 250 * Creates a new listener list that does not fail if listeners are added or removed twice. 251 * <p> 252 * Use of this list is discouraged. You should always use {@link #create()} in new implementations and check your listeners. 253 * @param <T> The listener type 254 * @return A new list. 255 * @since 11224 256 */ 257 public static <T> ListenerList<T> createUnchecked() { 258 return new UncheckedListenerList<>(); 259 } 260}