1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.tools;
|
---|
3 |
|
---|
4 | import java.lang.ref.WeakReference;
|
---|
5 | import java.text.MessageFormat;
|
---|
6 | import java.util.HashMap;
|
---|
7 | import java.util.Iterator;
|
---|
8 | import java.util.Objects;
|
---|
9 | import java.util.concurrent.CopyOnWriteArrayList;
|
---|
10 | import java.util.stream.Stream;
|
---|
11 |
|
---|
12 | /**
|
---|
13 | * This is a list of listeners. It does error checking and allows you to fire all listeners.
|
---|
14 | *
|
---|
15 | * @author Michael Zangl
|
---|
16 | * @param <T> The type of listener contained in this list.
|
---|
17 | * @since 10824
|
---|
18 | */
|
---|
19 | public class ListenerList<T> {
|
---|
20 | /**
|
---|
21 | * This is a function that can be invoked for every listener.
|
---|
22 | * @param <T> the listener type.
|
---|
23 | */
|
---|
24 | @FunctionalInterface
|
---|
25 | public interface EventFirerer<T> {
|
---|
26 | /**
|
---|
27 | * Should fire the event for the given listener.
|
---|
28 | * @param listener The listener to fire the event for.
|
---|
29 | */
|
---|
30 | void fire(T listener);
|
---|
31 | }
|
---|
32 |
|
---|
33 | private static final class WeakListener<T> {
|
---|
34 |
|
---|
35 | private final WeakReference<T> listener;
|
---|
36 |
|
---|
37 | WeakListener(T listener) {
|
---|
38 | this.listener = new WeakReference<>(listener);
|
---|
39 | }
|
---|
40 |
|
---|
41 | @Override
|
---|
42 | public boolean equals(Object obj) {
|
---|
43 | if (obj != null && obj.getClass() == WeakListener.class) {
|
---|
44 | return Objects.equals(listener.get(), ((WeakListener<?>) obj).listener.get());
|
---|
45 | } else {
|
---|
46 | return false;
|
---|
47 | }
|
---|
48 | }
|
---|
49 |
|
---|
50 | @Override
|
---|
51 | public int hashCode() {
|
---|
52 | T l = listener.get();
|
---|
53 | if (l == null) {
|
---|
54 | return 0;
|
---|
55 | } else {
|
---|
56 | return l.hashCode();
|
---|
57 | }
|
---|
58 | }
|
---|
59 |
|
---|
60 | @Override
|
---|
61 | public String toString() {
|
---|
62 | return "WeakListener [listener=" + listener + ']';
|
---|
63 | }
|
---|
64 | }
|
---|
65 |
|
---|
66 | private final CopyOnWriteArrayList<T> listeners = new CopyOnWriteArrayList<>();
|
---|
67 | private final CopyOnWriteArrayList<WeakListener<T>> weakListeners = new CopyOnWriteArrayList<>();
|
---|
68 |
|
---|
69 | protected ListenerList() {
|
---|
70 | // hide
|
---|
71 | }
|
---|
72 |
|
---|
73 | /**
|
---|
74 | * Adds a listener. The listener will not prevent the object from being garbage collected.
|
---|
75 | *
|
---|
76 | * This should be used with care. It is better to add good cleanup code.
|
---|
77 | * @param listener The listener.
|
---|
78 | */
|
---|
79 | public synchronized void addWeakListener(T listener) {
|
---|
80 | if (ensureNotInList(listener)) {
|
---|
81 | // clean the weak listeners, just to be sure...
|
---|
82 | while (weakListeners.remove(new WeakListener<T>(null))) {
|
---|
83 | // continue
|
---|
84 | }
|
---|
85 | weakListeners.add(new WeakListener<>(listener));
|
---|
86 | }
|
---|
87 | }
|
---|
88 |
|
---|
89 | /**
|
---|
90 | * Adds a listener.
|
---|
91 | * @param listener The listener to add.
|
---|
92 | */
|
---|
93 | public synchronized void addListener(T listener) {
|
---|
94 | if (ensureNotInList(listener)) {
|
---|
95 | listeners.add(listener);
|
---|
96 | }
|
---|
97 | }
|
---|
98 |
|
---|
99 | 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 ore 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 | }
|
---|