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}