EventListenerMap.java

package org.djutils.event;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.djutils.event.reference.Reference;
import org.djutils.event.rmi.RmiEventListener;
import org.djutils.exceptions.Throw;

/**
 * The EventListenerMap maps EventTypes on lists of References to EventListeners. The References can be Weak or Strong. The Map
 * can be serialized. When serializing, the References to RemoteEventListeners are not written as they are fully dependent on a
 * volatile network state that will almost certainly not be the same when the serialized map is read back.
 * <p>
 * Copyright (c) 2002-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
 * distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>. This class was
 * originally part of the DSOL project, see <a href="https://simulation.tudelft.nl/dsol/manual" target="_blank">
 * https://simulation.tudelft.nl/dsol/manual</a>.
 * </p>
 * @author <a href="https://www.linkedin.com/in/peterhmjacobs">Peter Jacobs </a>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 */
public final class EventListenerMap implements Serializable
{
    /** The default serial version UID for serializable classes. */
    private static final long serialVersionUID = 20140830L;

    /** The hasMap we map on. */
    private Map<EventType, List<Reference<EventListener>>> map =
            Collections.synchronizedMap(new LinkedHashMap<>());

    /**
     * Return the size of the EventListenerMap, i.e. the number of EventTypes that are registered.
     * @return int; the size of the EventListenerMap, i.e. the number of EventTypes that are registered
     */
    public int size()
    {
        return this.map.size();
    }

    /**
     * Clears the EventListenerMap.
     */
    public void clear()
    {
        this.map.clear();
    }

    /**
     * Return whether the EventListenerMap is empty.
     * @return boolean; whether the EventListenerMap is empty
     */
    public boolean isEmpty()
    {
        return this.map.isEmpty();
    }

    /**
     * Return whether the EventListenerMap contains the EventType as a key.
     * @param eventType EventType; the EventType key to search for
     * @return boolean; whether the EventListenerMap contains the EventType as a key
     */
    public boolean containsKey(final EventType eventType)
    {
        Throw.whenNull(eventType, "Cannot search for a null EventType");
        return this.map.containsKey(eventType);
    }

    /**
     * Return whether the EventListenerMap contains the eventListener as one of the subscribers.
     * @param eventListener EventListenerInterface; the EventListener value to search for
     * @return boolean; true if the EventListenerMap contains the eventListener as one of the subscribers; false otherwise
     */
    public boolean containsValue(final EventListener eventListener)
    {
        Throw.whenNull(eventListener, "Cannot search for a null EventListener");
        for (List<Reference<EventListener>> refList : this.map.values())
        {
            for (Reference<EventListener> ref : refList)
            {
                if (eventListener.equals(ref.get()))
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns whether the EventListenerMap contains the reference to the eventListener as one of the subscribers.
     * @param reference Reference&lt;EventListenerInterface&gt;; the reference pointer an EventListener to search for
     * @return boolean; true if the EventListenerMap contains the reference to the eventListener as one of the subscribers;
     *         false otherwise
     */
    public boolean containsValue(final Reference<EventListener> reference)
    {
        Throw.whenNull(reference, "Cannot search for a null reference");
        for (List<Reference<EventListener>> refList : this.map.values())
        {
            for (Reference<EventListener> ref : refList)
            {
                if (reference.equals(ref))
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns a safe copy of the collection of lists of references to EventListeners, i.e. all the listeners registered in the
     * map
     * @return Collection&lt;List&lt;Reference&lt;EventListenerInterface&gt;&gt;&gt;; a safe copy of the collection of lists of
     *         references to EventListeners, i.e. all the listeners registered in the map
     */
    public Collection<List<Reference<EventListener>>> values()
    {
        Collection<List<Reference<EventListener>>> result = new LinkedHashSet<>();
        for (List<Reference<EventListener>> list : this.map.values())
        {
            result.add(new ArrayList<>(list));
        }
        return result;
    }

    /**
     * Add all entries of the map to the EventListenerMap. The lists of listeners are added as a safe copy, so the list will not
     * be changed when the entries from copied map will be changed.
     * @param m EventListenerMap; the map with references to event listeners to add to the current EventListenerMap
     */
    public void putAll(final EventListenerMap m)
    {
        Throw.whenNull(m, "Cannot use putAll for a null map");
        for (Map.Entry<EventType, List<Reference<EventListener>>> entry : m.entrySet())
        {
            put(entry.getKey(), new ArrayList<>(entry.getValue()));
        }
    }

    /**
     * Returns the Set of Entry types holding pairs of a key (EventType) and a value (List of references to
     * EventListeners for that EventType). Note: this is a map with the real values, so not a safe copy. This entrySet
     * can be used to change the underlying map.
     * @return Set&lt;Map.Entry&lt;EventType, List&lt;Reference&lt;EventListenerInterface&gt;&gt;&gt;&gt;;the Set of
     *         Entry types holding pairs of a key (EventType) and a value (List of references to EventListeners for
     *         that EventType). Note: this is <b>not</b> a safe copy!
     */
    public Set<Map.Entry<EventType, List<Reference<EventListener>>>> entrySet()
    {
        return this.map.entrySet();
    }

    /**
     * Returns a safe copy of the Set of EventTypes for which listeners are registered.
     * @return Set&lt;EventType&gt;; a safe copy of the Set of EventType keys for which listeners are
     *         registered
     */
    public Set<EventType> keySet()
    {
        return new LinkedHashSet<EventType>(this.map.keySet());
    }

    /**
     * Returns the original List of references to EventListeners for the given EventType. Note: this is <b>not</b> a
     * safe copy, so the list is backed by the original data structure and will change when listeners are added or removed. The
     * method will return null when the EventType is not found.
     * @param key EventType; the eventType to look up the listeners for
     * @return List&lt;Reference&lt;EventListenerInterface&gt;; the List of references to EventListeners for the given
     *         EventType, or null when the EventType is not found. Note: this is <b>not</b> a safe copy.
     */
    public List<Reference<EventListener>> get(final EventType key)
    {
        Throw.whenNull(key, "Cannot use get for a null EventType key");
        return this.map.get(key);
    }

    /**
     * Remove the List of references to EventListeners for the given EventType.
     * @param key EventType; the eventType to remove the listeners for
     * @return List&lt;Reference&lt;EventListenerInterface&gt;&gt;; the removed List of references to EventListeners for the
     *         given EventType
     */
    public List<Reference<EventListener>> remove(final EventType key)
    {
        Throw.whenNull(key, "Cannot use remove for a null EventType key");
        return this.map.remove(key);
    }

    /**
     * Add the List of references to EventListeners for the given EventType to the underlying Map. A safe copy will be
     * added, so the original list will not be affected when listeners are removed or added, nor will the underlying map be
     * affected when the provided list is changed.
     * @param key EventType; the eventType to store the listeners for
     * @param value List&lt;Reference&lt;EventListenerInterface&gt;&gt;; the references to EventListeners to store for the given
     *            EventType
     * @return List&lt;Reference&lt;EventListenerInterface&gt;; the previous List of references to EventListeners for the given
     *         EventType, or null when there was no previous mapping
     */
    public List<Reference<EventListener>> put(final EventType key,
            final List<Reference<EventListener>> value)
    {
        Throw.whenNull(key, "Cannot use put with a null EventType key");
        Throw.whenNull(value, "Cannot use put with a null List as value");
        return this.map.put(key, new ArrayList<>(value));
    }

    /**
     * Write the EventListenerMap to a stream. RemoteEventListeners are not written, as they are fully dependent on the state of
     * the network, which might not be the same when the EventListenerMap is read back. Weak references and strong references
     * are both written to the stream.
     * @param out ObjectOutputStream; the output stream
     * @throws IOException on IOException
     */
    private synchronized void writeObject(final ObjectOutputStream out) throws IOException
    {
        Map<EventType, List<Reference<EventListener>>> outMap = new LinkedHashMap<>();
        for (Map.Entry<EventType, List<Reference<EventListener>>> entry : this.map.entrySet())
        {
            outMap.put(entry.getKey(), new ArrayList<>(entry.getValue()));
        }
        for (Entry<EventType, List<Reference<EventListener>>> entry : this.map.entrySet())
        {
            for (Reference<EventListener> reference : entry.getValue())
            {
                if (reference.get() instanceof RmiEventListener)
                {
                    outMap.get(entry.getKey()).remove(reference);
                }
            }
            if (outMap.get(entry.getKey()).isEmpty())
            {
                outMap.remove(entry.getKey());
            }
        }
        out.writeObject(outMap);
    }

    /**
     * Read an EventListenerMap from a stream and use it to replace the internal map.
     * @param in java.io.ObjectInputStream; the input stream
     * @throws IOException on IOException
     * @throws ClassNotFoundException on ClassNotFoundException
     */
    @SuppressWarnings("unchecked")
    private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
    {
        this.map = (LinkedHashMap<EventType, List<Reference<EventListener>>>) in.readObject();
    }

}