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