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