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.EventInterface; import org.djutils.event.EventListenerInterface; import org.djutils.event.EventProducer; import org.djutils.event.EventProducerInterface; import org.djutils.event.ref.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.
*
* For listening, four different ContextScope options exist: * * The listeners to be notified are determined with a regular expression. Examples of this regular expression are given below. * *

* Copyright (c) 2020-2021 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See * for project information https://simulation.tudelft.nl. The DSOL * project is distributed under a three-clause BSD-style license, which can be found at * * https://simulation.tudelft.nl/dsol/3.0/license.html. *

* @author Alexander Verbraeck */ public class ContextEventProducerImpl extends EventProducer implements EventListenerInterface { /** */ 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 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 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 Serializable getSourceId() { try { return this.parent.getSourceId(); } catch (RemoteException exception) { throw new RuntimeException(exception); } } /** {@inheritDoc} */ @Override public void notify(final EventInterface 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 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 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 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: *
    *
  • OBJECT_SCOPE: 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.
  • *
  • LEVEL_SCOPE: Suppose we are interested in changes to the objects registered directly 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.
  • *
  • LEVEL_OBJECT_SCOPE: Suppose we are interested in changes to the objects registered directly 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.
  • *
  • SUBTREE_SCOPE: 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.
  • *
* @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 EventListenerInterface; 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 EventListenerInterface listener, final String absolutePath, final ContextScope contextScope) throws RemoteException, NameNotFoundException, InvalidNameException, NotContextException, NamingException, NullPointerException { return addListener(listener, absolutePath, contextScope, EventProducerInterface.FIRST_POSITION, ReferenceType.STRONG); } /** * Add a listener for the provided scope to the BEGINNING of a queue of listeners. * @param listener EventListenerInterface; 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. * @see org.djutils.event.ref.Reference */ public boolean addListener(final EventListenerInterface listener, final String absolutePath, final ContextScope contextScope, final ReferenceType referenceType) throws RemoteException, NameNotFoundException, InvalidNameException, NotContextException, NamingException, NullPointerException { return addListener(listener, absolutePath, contextScope, EventProducerInterface.FIRST_POSITION, referenceType); } /** * Add a listener for the provided scope as strong reference to the specified position of a queue of listeners. * @param listener EventListenerInterface; 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 EventListenerInterface 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 EventListenerInterface; 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 * @see org.djutils.event.ref.Reference */ public boolean addListener(final EventListenerInterface 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 EventListenerInterface; 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 EventListenerInterface 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. *

* Copyright (c) 2020-2021 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. * See for project information https://simulation.tudelft.nl. * The DSOL project is distributed under a three-clause BSD-style license, which can be found at * * https://simulation.tudelft.nl/dsol/3.0/license.html. *

* @author Alexander Verbraeck */ protected final 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 EventListenerInterface listener; /** * Construct a pattern - listener pair. * @param pattern Pattern; the compiled pattern * @param listener EventListenerInterface; the registered listener for this pattern */ public PatternListener(Pattern pattern, EventListenerInterface listener) { super(); this.pattern = pattern; this.listener = listener; } /** * return the compiled pattern. * @return Pattern; the compiled pattern */ public final Pattern getPattern() { return this.pattern; } /** * Return the registered listener for this pattern. * @return EventListenerInterface; the registered listener for this pattern */ public final EventListenerInterface getListener() { return this.listener; } } }