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    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 or 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}