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 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 * Adds a listener. 092 * @param listener The listener to add. 093 */ 094 public synchronized void addListener(T listener) { 095 ensureNotInList(listener); 096 listeners.add(listener); 097 } 098 099 private void ensureNotInList(T listener) { 100 CheckParameterUtil.ensureParameterNotNull(listener, "listener"); 101 if (containsListener(listener)) { 102 failAdd(listener); 103 } 104 } 105 106 protected void failAdd(T listener) { 107 throw new IllegalArgumentException( 108 MessageFormat.format("Listener {0} (instance of {1}) was already registered.", listener, 109 listener.getClass().getName())); 110 } 111 112 private boolean containsListener(T listener) { 113 return listeners.contains(listener) || weakListeners.contains(new WeakListener<>(listener)); 114 } 115 116 /** 117 * Removes a listener. 118 * @param listener The listener to remove. 119 * @throws IllegalArgumentException if the listener was not registered before 120 */ 121 public synchronized void removeListener(T listener) { 122 if (!listeners.remove(listener) && !weakListeners.remove(new WeakListener<>(listener))) { 123 failRemove(listener); 124 } 125 } 126 127 protected void failRemove(T listener) { 128 throw new IllegalArgumentException( 129 MessageFormat.format("Listener {0} (instance of {1}) was not registered before or already removed.", 130 listener, listener.getClass().getName())); 131 } 132 133 /** 134 * Check if any listeners are registered. 135 * @return <code>true</code> if any are registered. 136 */ 137 public boolean hasListeners() { 138 return !listeners.isEmpty(); 139 } 140 141 /** 142 * Fires an event to every listener. 143 * @param eventFirerer The firerer to invoke the event method of the listener. 144 */ 145 public void fireEvent(EventFirerer<T> eventFirerer) { 146 for (T l : listeners) { 147 eventFirerer.fire(l); 148 } 149 for (Iterator<WeakListener<T>> iterator = weakListeners.iterator(); iterator.hasNext();) { 150 WeakListener<T> weakLink = iterator.next(); 151 T l = weakLink.listener.get(); 152 if (l != null) { 153 // cleanup during add() should be enough to not cause memory leaks 154 // therefore, we ignore null listeners. 155 eventFirerer.fire(l); 156 } 157 } 158 } 159 160 /** 161 * This is a special {@link ListenerList} that traces calls to the add/remove methods. This may cause memory leaks. 162 * @author Michael Zangl 163 * 164 * @param <T> The type of listener contained in this list 165 */ 166 public static class TracingListenerList<T> extends ListenerList<T> { 167 private final HashMap<T, StackTraceElement[]> listenersAdded = new HashMap<>(); 168 private final HashMap<T, StackTraceElement[]> listenersRemoved = new HashMap<>(); 169 170 protected TracingListenerList() { 171 // hidden 172 } 173 174 @Override 175 public synchronized void addListener(T listener) { 176 super.addListener(listener); 177 listenersRemoved.remove(listener); 178 listenersAdded.put(listener, Thread.currentThread().getStackTrace()); 179 } 180 181 @Override 182 public synchronized void addWeakListener(T listener) { 183 super.addWeakListener(listener); 184 listenersRemoved.remove(listener); 185 listenersAdded.put(listener, Thread.currentThread().getStackTrace()); 186 } 187 188 @Override 189 public synchronized void removeListener(T listener) { 190 super.removeListener(listener); 191 listenersAdded.remove(listener); 192 listenersRemoved.put(listener, Thread.currentThread().getStackTrace()); 193 } 194 195 @Override 196 protected void failAdd(T listener) { 197 Main.trace("Previous addition of the listener"); 198 dumpStack(listenersAdded.get(listener)); 199 super.failAdd(listener); 200 } 201 202 @Override 203 protected void failRemove(T listener) { 204 Main.trace("Previous removal of the listener"); 205 dumpStack(listenersRemoved.get(listener)); 206 super.failRemove(listener); 207 } 208 209 private static void dumpStack(StackTraceElement ... stackTraceElements) { 210 if (stackTraceElements == null) { 211 Main.trace(" - (no trace recorded)"); 212 } else { 213 Stream.of(stackTraceElements).limit(20).forEach( 214 e -> Main.trace(e.getClassName() + "." + e.getMethodName() + " line " + e.getLineNumber())); 215 } 216 } 217 } 218 219 /** 220 * Create a new listener list 221 * @param <T> The listener type the list should hold. 222 * @return A new list. A tracing list is created if trace is enabled. 223 */ 224 public static <T> ListenerList<T> create() { 225 if (Main.isTraceEnabled()) { 226 return new TracingListenerList<>(); 227 } else { 228 return new ListenerList<>(); 229 } 230 } 231}