ContextEventProducerImpl.java

package nl.tudelft.simulation.naming.context.event;

import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import javax.naming.InvalidNameException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.NotContextException;

import org.djutils.event.Event;
import org.djutils.event.EventListener;
import org.djutils.event.LocalEventProducer;
import org.djutils.event.reference.ReferenceType;
import org.djutils.exceptions.Throw;

import nl.tudelft.simulation.naming.context.ContextInterface;

/**
 * ContextEventProducerImpl carries out the implementation for the EventContext classes. The class registers as a listener on
 * the root of the InitialEventContext or the RemoteEventContext. Whenever sub-contexts are added (these will typically not be
 * of type EventContext, but rather of type JvmContext, FileContext, RemoteContext, or other), this class also registers as a
 * listener on these sub-contexts. Thereby, it remains aware of all changes happening in the sub-contexts of which it may be
 * notified.<br>
 * <br>
 * For listening, four different ContextScope options exist:
 * <ul>
 * <li><b>OBJECT_SCOPE</b>: listen to the changes in the object. OBJECT_SCOPE can be applied to regular objects and to context
 * objects. When the object is at a leaf position in the tree, OBJECT_CHANGED events will be fired to the listener(s) on a
 * change of the leaf object, and OBJECT_REMOVED events when the leaf is removed from the context. When the object is a context,
 * OBJECT_REMOVED is fired when the sub-context is removed from its parent context.</li>
 * <li><b>LEVEL_SCOPE</b>: fires events when changes happen in the provided context. LEVEL_SCOPE can only be applied to context
 * objects. OBJECT_ADDED is fired when an object (context or leaf) is added to the context; OBJECT_REMOVED when an object
 * (context or leaf) is removed from the context; OBJECT_CHANGED when one of the leaf objects present in the provided context
 * had changes in its content. OBJECT_REMOVED is <b>not</b> fired when the sub-context is removed from the parent context.</li>
 * <li><b>LEVEL_OBJECT_SCOPE</b>: fires events when changes happen in the provided context. LEVEL_OBJECT_SCOPE can only be
 * applied to context objects. OBJECT_ADDED is fired when an object (context or leaf) is added to the context; OBJECT_REMOVED
 * when an object (context or leaf) is removed from the context; OBJECT_CHANGED when one of the leaf objects present in the
 * provided context had changes in its content. OBJECT_REMOVED <b>is also</b> fired when the sub-context is removed from the
 * parent context.</li>
 * <li><b>SUBTREE_SCOPE</b>: fires events when changes happen in the provided context, and any context deeper in the tree
 * descending from the provided context. SUBTREE_SCOPE can only be applied to context objects. OBJECT_ADDED is fired when an
 * object (context or leaf) is added to the context, or any of its descendant contexts; OBJECT_REMOVED when an object (context
 * or leaf) is removed from the context or a descendant context; OBJECT_CHANGED when one of the leaf objects present in the
 * provided context or any descendant context had changes in its content. OBJECT_REMOVED <b>is also</b> fired when the
 * sub-context is removed from the parent context.</li>
 * </ul>
 * The listeners to be notified are determined with a regular expression. Examples of this regular expression are given below.
 * <ul>
 * <li><b>OBJECT_SCOPE</b>: Suppose we are interested in changes to the object registered under "/simulation1/sub1/myobject". In
 * that case, the regular expression is "/simulation1/sub1/myobject" as we are only interested in changes to the object
 * registered under this key.</li>
 * <li><b>LEVEL_SCOPE</b>: Suppose we are interested in changes to the objects registered <i>directly</i> under
 * "/simulation1/sub1", excluding the sub1 context itself. In that case, the regular expression is "/simulation1/sub1/[^/]+$" as
 * we are only interested in changes to the objects registered under this key, so minimally one character after the key that
 * cannot be a forward slash, as that would indicate a subcontext.</li>
 * <li><b>LEVEL_OBJECT_SCOPE</b>: Suppose we are interested in changes to the objects registered <i>directly</i> under
 * "/simulation1/sub1", including the "/simulation1/sub1" context itself. In that case, the regular expression is
 * "/simulation1/sub1(/[^/]*$)?" as we are interested in changes to the objects directly registered under this key, so minimally
 * one character after the key that cannot be a forward slash. The context "sub1" itself is also included, with or without a
 * forward slash at the end.</li>
 * <li><b>SUBTREE_SCOPE</b>: Suppose we are interested in changes to the objects registered in the total subtree under
 * "/simulation1/sub1", including the "/simulation1/sub1" context itself. In that case, the regular expression is
 * "/simulation1/sub1(/.*)?". The context "sub1" itself is also included, with or without a forward slash at the end.</li>
 * </ul>
 * <p>
 * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
 * project is distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
 * https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
 * </p>
 * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
 */
public class ContextEventProducerImpl extends LocalEventProducer implements EventListener
{
    /** */
    private static final long serialVersionUID = 20200209L;

    /**
     * The registry for the listeners. The key of the map is made from the given path to listen followed by a hash sign and the
     * toString of the used ContextScope enum. String has a cached hash.
     */
    private Map<String, PatternListener> regExpListenerMap = new LinkedHashMap<>();

    /**
     * The records for the scope listeners so we can find them back for removal. The key of the map is made from the given path
     * to listen followed by a hash sign and the toString of the used ContextScope enum. String has a cached hash.
     */
    private Map<String, PatternListener> registryMap = new LinkedHashMap<>();

    /** the EventContext for which we do the work. */
    private final EventContext parent;

    /**
     * Create the ContextEventProducerImpl and link to the parent class.
     * @param parent EventContext; the EventContext for which we do the work
     * @throws RemoteException on network error
     */
    public ContextEventProducerImpl(final EventContext parent) throws RemoteException
    {
        super();
        this.parent = parent;

        // we subscribe ourselves to the OBJECT_ADDED, OBJECT_REMOVED and OBJECT_CHANGED events of the parent
        this.parent.addListener(this, ContextInterface.OBJECT_ADDED_EVENT, ReferenceType.WEAK);
        this.parent.addListener(this, ContextInterface.OBJECT_REMOVED_EVENT, ReferenceType.WEAK);
        this.parent.addListener(this, ContextInterface.OBJECT_CHANGED_EVENT, ReferenceType.WEAK);
    }

    /** {@inheritDoc} */
    @Override
    public void notify(final Event event) throws RemoteException
    {
        Object[] content = (Object[]) event.getContent();
        if (event.getType().equals(ContextInterface.OBJECT_ADDED_EVENT))
        {
            if (content[2] instanceof ContextInterface)
            {
                ContextInterface context = (ContextInterface) content[2];
                context.addListener(this, ContextInterface.OBJECT_ADDED_EVENT);
                context.addListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
                context.addListener(this, ContextInterface.OBJECT_CHANGED_EVENT);
            }
            for (Entry<String, PatternListener> entry : this.regExpListenerMap.entrySet())
            {
                String path = (String) content[0] + ContextInterface.SEPARATOR + (String) content[1];
                if (entry.getValue().getPattern().matcher(path).matches())
                {
                    entry.getValue().getListener().notify(event);
                }
            }
        }
        else if (event.getType().equals(ContextInterface.OBJECT_REMOVED_EVENT))
        {
            if (content[2] instanceof ContextInterface)
            {
                ContextInterface context = (ContextInterface) content[2];
                context.removeListener(this, ContextInterface.OBJECT_ADDED_EVENT);
                context.removeListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
                context.removeListener(this, ContextInterface.OBJECT_CHANGED_EVENT);
            }
            for (Entry<String, PatternListener> entry : this.regExpListenerMap.entrySet())
            {
                String path = (String) content[0] + ContextInterface.SEPARATOR + (String) content[1];
                if (entry.getValue().getPattern().matcher(path).matches())
                {
                    entry.getValue().getListener().notify(event);
                }
            }
        }
        else if (event.getType().equals(ContextInterface.OBJECT_CHANGED_EVENT))
        {
            for (Entry<String, PatternListener> entry : this.regExpListenerMap.entrySet())
            {
                String path = (String) content[0] + ContextInterface.SEPARATOR + (String) content[1];
                if (entry.getValue().getPattern().matcher(path).matches())
                {
                    entry.getValue().getListener().notify(event);
                }
            }
        }
    }

    /**
     * Make a key consisting of the full path of the subcontext (without the trailing slash) or object in the context tree,
     * followed by a hash code (#) and the context scope string (OBJECT_SCOPE, LEVEL_SCOPE, LEVEL_OBJECT_SCOPE, or
     * SUBTREE_SCOPE).
     * @param absolutePath String; the path for which the key has to be made. The path can point to an object or a subcontext
     * @param contextScope ContextScope; the scope for which the key has to be made
     * @return a concatenation of the path, a hash (#) and the context scope
     */
    protected String makeRegistryKey(final String absolutePath, final ContextScope contextScope)
    {
        String key = absolutePath;
        if (key.endsWith("/"))
        {
            key = key.substring(0, key.length() - 1);
        }
        key += "#" + contextScope.name();
        return key;
    }

    /**
     * Make a regular expression that matches the right paths to be matched. The regular expression per scope is:
     * <ul>
     * <li><b>OBJECT_SCOPE</b>: Suppose we are interested in changes to the object registered under
     * "/simulation1/sub1/myobject". In that case, the regular expression is "/simulation1/sub1/myobject" as we are only
     * interested in changes to the object registered under this key.</li>
     * <li><b>LEVEL_SCOPE</b>: Suppose we are interested in changes to the objects registered <i>directly</i> under
     * "/simulation1/sub1", excluding the sub1 context itself. In that case, the regular expression is
     * "/simulation1/sub1/[^/]+$" as we are only interested in changes to the objects registered under this key, so minimally
     * one character after the key that cannot be a forward slash, as that would indicate a subcontext.</li>
     * <li><b>LEVEL_OBJECT_SCOPE</b>: Suppose we are interested in changes to the objects registered <i>directly</i> under
     * "/simulation1/sub1", including the "/simulation1/sub1" context itself. In that case, the regular expression is
     * "/simulation1/sub1(/[^/]*$)?" as we are interested in changes to the objects directly registered under this key, so
     * minimally one character after the key that cannot be a forward slash. The context "sub1" itself is also included, with or
     * without a forward slash at the end.</li>
     * <li><b>SUBTREE_SCOPE</b>: Suppose we are interested in changes to the objects registered in the total subtree under
     * "/simulation1/sub1", including the "/simulation1/sub1" context itself. In that case, the regular expression is
     * "/simulation1/sub1(/.*)?". The context "sub1" itself is also included, with or without a forward slash at the end.</li>
     * </ul>
     * @param absolutePath String; the path for which the key has to be made. The path can point to an object or a subcontext
     * @param contextScope ContextScope; the scope for which the key has to be made
     * @return a concatenation of the path, a hash (#) and the context scope
     */
    protected String makeRegex(final String absolutePath, final ContextScope contextScope)
    {
        String key = absolutePath;
        if (key.endsWith("/"))
        {
            key = key.substring(0, key.length() - 1);
        }
        switch (contextScope)
        {
            case LEVEL_SCOPE:
                key += "/[^/]+$";
                break;

            case LEVEL_OBJECT_SCOPE:
                key += "(/[^/]*$)?";
                break;

            case SUBTREE_SCOPE:
                key += "(/.*)?";
                break;

            default:
                break;
        }
        return key;
    }

    /**
     * Add a listener for the provided scope as strong reference to the BEGINNING of a queue of listeners.
     * @param listener EventListener; the listener which is interested at events of eventType.
     * @param absolutePath String; the absolute path of the context or object to subscribe to
     * @param contextScope ContextScope; the part of the tree that the listener is aimed at (current node, current node and
     *            keys, subtree).
     * @return the success of adding the listener. If a listener was already added false is returned.
     * @throws NameNotFoundException when the absolutePath could not be found in the parent context, or when an intermediate
     *             context does not exist
     * @throws InvalidNameException when the scope is OBJECT_SCOPE, but the key points to a (sub)context
     * @throws NotContextException when the scope is LEVEL_SCOPE, OBJECT_LEVEL_SCOPE or SUBTREE_SCOPE, and the key points to an
     *             ordinary object
     * @throws NamingException as an overarching exception for context errors
     * @throws NullPointerException when one of the arguments is null
     * @throws RemoteException if a network connection failure occurs.
     */
    public boolean addListener(final EventListener listener, final String absolutePath, final ContextScope contextScope)
            throws RemoteException, NameNotFoundException, InvalidNameException, NotContextException, NamingException,
            NullPointerException
    {
        return addListener(listener, absolutePath, contextScope, LocalEventProducer.FIRST_POSITION, ReferenceType.STRONG);
    }

    /**
     * Add a listener for the provided scope to the BEGINNING of a queue of listeners.
     * @param listener EventListener; the listener which is interested at events of eventType.
     * @param absolutePath String; the absolute path of the context or object to subscribe to
     * @param contextScope ContextScope; the part of the tree that the listener is aimed at (current node, current node and
     *            keys, subtree).
     * @param referenceType ReferenceType; whether the listener is added as a strong or as a weak reference.
     * @return the success of adding the listener. If a listener was already added false is returned.
     * @throws NameNotFoundException when the absolutePath could not be found in the parent context, or when an intermediate
     *             context does not exist
     * @throws InvalidNameException when the scope is OBJECT_SCOPE, but the key points to a (sub)context
     * @throws NotContextException when the scope is LEVEL_SCOPE, OBJECT_LEVEL_SCOPE or SUBTREE_SCOPE, and the key points to an
     *             ordinary object
     * @throws NamingException as an overarching exception for context errors
     * @throws NullPointerException when one of the arguments is null
     * @throws RemoteException if a network connection failure occurs.
     */
    public boolean addListener(final EventListener listener, final String absolutePath, final ContextScope contextScope,
            final ReferenceType referenceType) throws RemoteException, NameNotFoundException, InvalidNameException,
            NotContextException, NamingException, NullPointerException
    {
        return addListener(listener, absolutePath, contextScope, LocalEventProducer.FIRST_POSITION, referenceType);
    }

    /**
     * Add a listener for the provided scope as strong reference to the specified position of a queue of listeners.
     * @param listener EventListener; the listener which is interested at events of eventType.
     * @param absolutePath String; the absolute path of the context or object to subscribe to
     * @param contextScope ContextScope; the part of the tree that the listener is aimed at (current node, current node and
     *            keys, subtree).
     * @param position int; the position of the listener in the queue.
     * @return the success of adding the listener. If a listener was already added, or an illegal position is provided false is
     *         returned.
     * @throws NameNotFoundException when the absolutePath could not be found in the parent context, or when an intermediate
     *             context does not exist
     * @throws InvalidNameException when the scope is OBJECT_SCOPE, but the key points to a (sub)context
     * @throws NotContextException when the scope is LEVEL_SCOPE, OBJECT_LEVEL_SCOPE or SUBTREE_SCOPE, and the key points to an
     *             ordinary object
     * @throws NamingException as an overarching exception for context errors
     * @throws NullPointerException when one of the arguments is null
     * @throws RemoteException if a network connection failure occurs.
     */
    public boolean addListener(final EventListener listener, final String absolutePath, final ContextScope contextScope,
            final int position) throws RemoteException, NameNotFoundException, InvalidNameException, NotContextException,
            NamingException, NullPointerException
    {
        return addListener(listener, absolutePath, contextScope, position, ReferenceType.STRONG);
    }

    /**
     * Add a listener for the provided scope to the specified position of a queue of listeners.
     * @param listener EventListener; which is interested at certain events,
     * @param absolutePath String; the absolute path of the context or object to subscribe to
     * @param contextScope ContextScope; the part of the tree that the listener is aimed at (current node, current node and
     *            keys, subtree).
     * @param position int; the position of the listener in the queue
     * @param referenceType ReferenceType; whether the listener is added as a strong or as a weak reference.
     * @return the success of adding the listener. If a listener was already added or an illegal position is provided false is
     *         returned.
     * @throws InvalidNameException when the path does not start with a slash
     * @throws NullPointerException when one of the arguments is null
     * @throws RemoteException if a network connection failure occurs
     */
    public boolean addListener(final EventListener listener, final String absolutePath, final ContextScope contextScope,
            final int position, final ReferenceType referenceType)
            throws RemoteException, InvalidNameException, NullPointerException
    {
        Throw.when(listener == null, NullPointerException.class, "listener cannot be null");
        Throw.when(absolutePath == null, NullPointerException.class, "absolutePath cannot be null");
        Throw.when(contextScope == null, NullPointerException.class, "contextScope cannot be null");
        Throw.when(referenceType == null, NullPointerException.class, "referenceType cannot be null");
        Throw.when(!absolutePath.startsWith("/"), InvalidNameException.class, "absolute path %s does not start with '/'",
                absolutePath);

        String registryKey = makeRegistryKey(absolutePath, contextScope);
        boolean added = !this.registryMap.containsKey(registryKey);
        String regex = makeRegex(absolutePath, contextScope);
        PatternListener patternListener = new PatternListener(Pattern.compile(regex), listener);
        this.regExpListenerMap.put(registryKey, patternListener);
        this.registryMap.put(registryKey, patternListener);
        return added;
    }

    /**
     * Remove the subscription of a listener for the provided scope for a specific event.
     * @param listener EventListener; which is no longer interested.
     * @param absolutePath String; the absolute path of the context or object to subscribe to
     * @param contextScope ContextScope;the scope which is of no interest any more.
     * @return the success of removing the listener. If a listener was not subscribed false is returned.
     * @throws InvalidNameException when the path does not start with a slash
     * @throws NullPointerException when one of the arguments is null
     * @throws RemoteException if a network connection failure occurs
     */
    public boolean removeListener(final EventListener listener, final String absolutePath, final ContextScope contextScope)
            throws RemoteException, InvalidNameException, NullPointerException
    {
        Throw.when(listener == null, NullPointerException.class, "listener cannot be null");
        Throw.when(absolutePath == null, NullPointerException.class, "absolutePath cannot be null");
        Throw.when(contextScope == null, NullPointerException.class, "contextScope cannot be null");
        Throw.when(!absolutePath.startsWith("/"), InvalidNameException.class, "absolute path %s does not start with '/'",
                absolutePath);

        String registryKey = makeRegistryKey(absolutePath, contextScope);
        boolean removed = this.registryMap.containsKey(registryKey);
        this.regExpListenerMap.remove(registryKey);
        this.registryMap.remove(registryKey);
        return removed;
    }

    /**
     * Pair of regular expression pattern and event listener.
     * <p>
     * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
     * See for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>.
     * The DSOL project is distributed under a three-clause BSD-style license, which can be found at
     * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
     * https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
     * </p>
     * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
     */
    public class PatternListener implements Serializable
    {
        /** */
        private static final long serialVersionUID = 20200210L;

        /** the compiled regular expression pattern. */
        private final Pattern pattern;

        /** the event listener for this pattern. */
        private final EventListener listener;

        /**
         * Construct a pattern - listener pair.
         * @param pattern Pattern; the compiled pattern
         * @param listener EventListener; the registered listener for this pattern
         */
        public PatternListener(final Pattern pattern, final EventListener listener)
        {
            super();
            this.pattern = pattern;
            this.listener = listener;
        }

        /**
         * return the compiled pattern.
         * @return Pattern; the compiled pattern
         */
        public Pattern getPattern()
        {
            return this.pattern;
        }

        /**
         * Return the registered listener for this pattern.
         * @return EventListener; the registered listener for this pattern
         */
        public EventListener getListener()
        {
            return this.listener;
        }
    }
}