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}