View Javadoc
1   package nl.tudelft.simulation.naming.context.event;
2   
3   import java.io.Serializable;
4   import java.rmi.RemoteException;
5   import java.util.LinkedHashMap;
6   import java.util.Map;
7   import java.util.Map.Entry;
8   import java.util.regex.Pattern;
9   
10  import javax.naming.InvalidNameException;
11  import javax.naming.NameNotFoundException;
12  import javax.naming.NamingException;
13  import javax.naming.NotContextException;
14  
15  import org.djutils.event.Event;
16  import org.djutils.event.EventListener;
17  import org.djutils.event.LocalEventProducer;
18  import org.djutils.event.reference.ReferenceType;
19  import org.djutils.exceptions.Throw;
20  
21  import nl.tudelft.simulation.naming.context.ContextInterface;
22  
23  /**
24   * ContextEventProducerImpl carries out the implementation for the EventContext classes. The class registers as a listener on
25   * the root of the InitialEventContext or the RemoteEventContext. Whenever sub-contexts are added (these will typically not be
26   * of type EventContext, but rather of type JvmContext, FileContext, RemoteContext, or other), this class also registers as a
27   * listener on these sub-contexts. Thereby, it remains aware of all changes happening in the sub-contexts of which it may be
28   * notified.<br>
29   * <br>
30   * For listening, four different ContextScope options exist:
31   * <ul>
32   * <li><b>OBJECT_SCOPE</b>: listen to the changes in the object. OBJECT_SCOPE can be applied to regular objects and to context
33   * objects. When the object is at a leaf position in the tree, OBJECT_CHANGED events will be fired to the listener(s) on a
34   * change of the leaf object, and OBJECT_REMOVED events when the leaf is removed from the context. When the object is a context,
35   * OBJECT_REMOVED is fired when the sub-context is removed from its parent context.</li>
36   * <li><b>LEVEL_SCOPE</b>: fires events when changes happen in the provided context. LEVEL_SCOPE can only be applied to context
37   * objects. OBJECT_ADDED is fired when an object (context or leaf) is added to the context; OBJECT_REMOVED when an object
38   * (context or leaf) is removed from the context; OBJECT_CHANGED when one of the leaf objects present in the provided context
39   * had changes in its content. OBJECT_REMOVED is <b>not</b> fired when the sub-context is removed from the parent context.</li>
40   * <li><b>LEVEL_OBJECT_SCOPE</b>: fires events when changes happen in the provided context. LEVEL_OBJECT_SCOPE can only be
41   * applied to context objects. OBJECT_ADDED is fired when an object (context or leaf) is added to the context; OBJECT_REMOVED
42   * when an object (context or leaf) is removed from the context; OBJECT_CHANGED when one of the leaf objects present in the
43   * provided context had changes in its content. OBJECT_REMOVED <b>is also</b> fired when the sub-context is removed from the
44   * parent context.</li>
45   * <li><b>SUBTREE_SCOPE</b>: fires events when changes happen in the provided context, and any context deeper in the tree
46   * descending from the provided context. SUBTREE_SCOPE can only be applied to context objects. OBJECT_ADDED is fired when an
47   * object (context or leaf) is added to the context, or any of its descendant contexts; OBJECT_REMOVED when an object (context
48   * or leaf) is removed from the context or a descendant context; OBJECT_CHANGED when one of the leaf objects present in the
49   * provided context or any descendant context had changes in its content. OBJECT_REMOVED <b>is also</b> fired when the
50   * sub-context is removed from the parent context.</li>
51   * </ul>
52   * The listeners to be notified are determined with a regular expression. Examples of this regular expression are given below.
53   * <ul>
54   * <li><b>OBJECT_SCOPE</b>: Suppose we are interested in changes to the object registered under "/simulation1/sub1/myobject". In
55   * that case, the regular expression is "/simulation1/sub1/myobject" as we are only interested in changes to the object
56   * registered under this key.</li>
57   * <li><b>LEVEL_SCOPE</b>: Suppose we are interested in changes to the objects registered <i>directly</i> under
58   * "/simulation1/sub1", excluding the sub1 context itself. In that case, the regular expression is "/simulation1/sub1/[^/]+$" as
59   * we are only interested in changes to the objects registered under this key, so minimally one character after the key that
60   * cannot be a forward slash, as that would indicate a subcontext.</li>
61   * <li><b>LEVEL_OBJECT_SCOPE</b>: Suppose we are interested in changes to the objects registered <i>directly</i> under
62   * "/simulation1/sub1", including the "/simulation1/sub1" context itself. In that case, the regular expression is
63   * "/simulation1/sub1(/[^/]*$)?" as we are interested in changes to the objects directly registered under this key, so minimally
64   * one character after the key that cannot be a forward slash. The context "sub1" itself is also included, with or without a
65   * forward slash at the end.</li>
66   * <li><b>SUBTREE_SCOPE</b>: Suppose we are interested in changes to the objects registered in the total subtree under
67   * "/simulation1/sub1", including the "/simulation1/sub1" context itself. In that case, the regular expression is
68   * "/simulation1/sub1(/.*)?". The context "sub1" itself is also included, with or without a forward slash at the end.</li>
69   * </ul>
70   * <p>
71   * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
72   * for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
73   * project is distributed under a three-clause BSD-style license, which can be found at
74   * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
75   * https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
76   * </p>
77   * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
78   */
79  public class ContextEventProducerImpl extends LocalEventProducer implements EventListener
80  {
81      /** */
82      private static final long serialVersionUID = 20200209L;
83  
84      /**
85       * 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
86       * toString of the used ContextScope enum. String has a cached hash.
87       */
88      private Map<String, PatternListener> regExpListenerMap = new LinkedHashMap<>();
89  
90      /**
91       * 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
92       * to listen followed by a hash sign and the toString of the used ContextScope enum. String has a cached hash.
93       */
94      private Map<String, PatternListener> registryMap = new LinkedHashMap<>();
95  
96      /** the EventContext for which we do the work. */
97      private final EventContext parent;
98  
99      /**
100      * Create the ContextEventProducerImpl and link to the parent class.
101      * @param parent EventContext; the EventContext for which we do the work
102      * @throws RemoteException on network error
103      */
104     public ContextEventProducerImpl(final EventContext parent) throws RemoteException
105     {
106         super();
107         this.parent = parent;
108 
109         // we subscribe ourselves to the OBJECT_ADDED, OBJECT_REMOVED and OBJECT_CHANGED events of the parent
110         this.parent.addListener(this, ContextInterface.OBJECT_ADDED_EVENT, ReferenceType.WEAK);
111         this.parent.addListener(this, ContextInterface.OBJECT_REMOVED_EVENT, ReferenceType.WEAK);
112         this.parent.addListener(this, ContextInterface.OBJECT_CHANGED_EVENT, ReferenceType.WEAK);
113     }
114 
115     /** {@inheritDoc} */
116     @Override
117     public void notify(final Event event) throws RemoteException
118     {
119         Object[] content = (Object[]) event.getContent();
120         if (event.getType().equals(ContextInterface.OBJECT_ADDED_EVENT))
121         {
122             if (content[2] instanceof ContextInterface)
123             {
124                 ContextInterface context = (ContextInterface) content[2];
125                 context.addListener(this, ContextInterface.OBJECT_ADDED_EVENT);
126                 context.addListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
127                 context.addListener(this, ContextInterface.OBJECT_CHANGED_EVENT);
128             }
129             for (Entry<String, PatternListener> entry : this.regExpListenerMap.entrySet())
130             {
131                 String path = (String) content[0] + ContextInterface.SEPARATOR + (String) content[1];
132                 if (entry.getValue().getPattern().matcher(path).matches())
133                 {
134                     entry.getValue().getListener().notify(event);
135                 }
136             }
137         }
138         else if (event.getType().equals(ContextInterface.OBJECT_REMOVED_EVENT))
139         {
140             if (content[2] instanceof ContextInterface)
141             {
142                 ContextInterface context = (ContextInterface) content[2];
143                 context.removeListener(this, ContextInterface.OBJECT_ADDED_EVENT);
144                 context.removeListener(this, ContextInterface.OBJECT_REMOVED_EVENT);
145                 context.removeListener(this, ContextInterface.OBJECT_CHANGED_EVENT);
146             }
147             for (Entry<String, PatternListener> entry : this.regExpListenerMap.entrySet())
148             {
149                 String path = (String) content[0] + ContextInterface.SEPARATOR + (String) content[1];
150                 if (entry.getValue().getPattern().matcher(path).matches())
151                 {
152                     entry.getValue().getListener().notify(event);
153                 }
154             }
155         }
156         else if (event.getType().equals(ContextInterface.OBJECT_CHANGED_EVENT))
157         {
158             for (Entry<String, PatternListener> entry : this.regExpListenerMap.entrySet())
159             {
160                 String path = (String) content[0] + ContextInterface.SEPARATOR + (String) content[1];
161                 if (entry.getValue().getPattern().matcher(path).matches())
162                 {
163                     entry.getValue().getListener().notify(event);
164                 }
165             }
166         }
167     }
168 
169     /**
170      * Make a key consisting of the full path of the subcontext (without the trailing slash) or object in the context tree,
171      * followed by a hash code (#) and the context scope string (OBJECT_SCOPE, LEVEL_SCOPE, LEVEL_OBJECT_SCOPE, or
172      * SUBTREE_SCOPE).
173      * @param absolutePath String; the path for which the key has to be made. The path can point to an object or a subcontext
174      * @param contextScope ContextScope; the scope for which the key has to be made
175      * @return a concatenation of the path, a hash (#) and the context scope
176      */
177     protected String makeRegistryKey(final String absolutePath, final ContextScope contextScope)
178     {
179         String key = absolutePath;
180         if (key.endsWith("/"))
181         {
182             key = key.substring(0, key.length() - 1);
183         }
184         key += "#" + contextScope.name();
185         return key;
186     }
187 
188     /**
189      * Make a regular expression that matches the right paths to be matched. The regular expression per scope is:
190      * <ul>
191      * <li><b>OBJECT_SCOPE</b>: Suppose we are interested in changes to the object registered under
192      * "/simulation1/sub1/myobject". In that case, the regular expression is "/simulation1/sub1/myobject" as we are only
193      * interested in changes to the object registered under this key.</li>
194      * <li><b>LEVEL_SCOPE</b>: Suppose we are interested in changes to the objects registered <i>directly</i> under
195      * "/simulation1/sub1", excluding the sub1 context itself. In that case, the regular expression is
196      * "/simulation1/sub1/[^/]+$" as we are only interested in changes to the objects registered under this key, so minimally
197      * one character after the key that cannot be a forward slash, as that would indicate a subcontext.</li>
198      * <li><b>LEVEL_OBJECT_SCOPE</b>: Suppose we are interested in changes to the objects registered <i>directly</i> under
199      * "/simulation1/sub1", including the "/simulation1/sub1" context itself. In that case, the regular expression is
200      * "/simulation1/sub1(/[^/]*$)?" as we are interested in changes to the objects directly registered under this key, so
201      * minimally one character after the key that cannot be a forward slash. The context "sub1" itself is also included, with or
202      * without a forward slash at the end.</li>
203      * <li><b>SUBTREE_SCOPE</b>: Suppose we are interested in changes to the objects registered in the total subtree under
204      * "/simulation1/sub1", including the "/simulation1/sub1" context itself. In that case, the regular expression is
205      * "/simulation1/sub1(/.*)?". The context "sub1" itself is also included, with or without a forward slash at the end.</li>
206      * </ul>
207      * @param absolutePath String; the path for which the key has to be made. The path can point to an object or a subcontext
208      * @param contextScope ContextScope; the scope for which the key has to be made
209      * @return a concatenation of the path, a hash (#) and the context scope
210      */
211     protected String makeRegex(final String absolutePath, final ContextScope contextScope)
212     {
213         String key = absolutePath;
214         if (key.endsWith("/"))
215         {
216             key = key.substring(0, key.length() - 1);
217         }
218         switch (contextScope)
219         {
220             case LEVEL_SCOPE:
221                 key += "/[^/]+$";
222                 break;
223 
224             case LEVEL_OBJECT_SCOPE:
225                 key += "(/[^/]*$)?";
226                 break;
227 
228             case SUBTREE_SCOPE:
229                 key += "(/.*)?";
230                 break;
231 
232             default:
233                 break;
234         }
235         return key;
236     }
237 
238     /**
239      * Add a listener for the provided scope as strong reference to the BEGINNING of a queue of listeners.
240      * @param listener EventListener; the listener which is interested at events of eventType.
241      * @param absolutePath String; the absolute path of the context or object to subscribe to
242      * @param contextScope ContextScope; the part of the tree that the listener is aimed at (current node, current node and
243      *            keys, subtree).
244      * @return the success of adding the listener. If a listener was already added false is returned.
245      * @throws NameNotFoundException when the absolutePath could not be found in the parent context, or when an intermediate
246      *             context does not exist
247      * @throws InvalidNameException when the scope is OBJECT_SCOPE, but the key points to a (sub)context
248      * @throws NotContextException when the scope is LEVEL_SCOPE, OBJECT_LEVEL_SCOPE or SUBTREE_SCOPE, and the key points to an
249      *             ordinary object
250      * @throws NamingException as an overarching exception for context errors
251      * @throws NullPointerException when one of the arguments is null
252      * @throws RemoteException if a network connection failure occurs.
253      */
254     public boolean addListener(final EventListener listener, final String absolutePath, final ContextScope contextScope)
255             throws RemoteException, NameNotFoundException, InvalidNameException, NotContextException, NamingException,
256             NullPointerException
257     {
258         return addListener(listener, absolutePath, contextScope, LocalEventProducer.FIRST_POSITION, ReferenceType.STRONG);
259     }
260 
261     /**
262      * Add a listener for the provided scope to the BEGINNING of a queue of listeners.
263      * @param listener EventListener; the listener which is interested at events of eventType.
264      * @param absolutePath String; the absolute path of the context or object to subscribe to
265      * @param contextScope ContextScope; the part of the tree that the listener is aimed at (current node, current node and
266      *            keys, subtree).
267      * @param referenceType ReferenceType; whether the listener is added as a strong or as a weak reference.
268      * @return the success of adding the listener. If a listener was already added false is returned.
269      * @throws NameNotFoundException when the absolutePath could not be found in the parent context, or when an intermediate
270      *             context does not exist
271      * @throws InvalidNameException when the scope is OBJECT_SCOPE, but the key points to a (sub)context
272      * @throws NotContextException when the scope is LEVEL_SCOPE, OBJECT_LEVEL_SCOPE or SUBTREE_SCOPE, and the key points to an
273      *             ordinary object
274      * @throws NamingException as an overarching exception for context errors
275      * @throws NullPointerException when one of the arguments is null
276      * @throws RemoteException if a network connection failure occurs.
277      */
278     public boolean addListener(final EventListener listener, final String absolutePath, final ContextScope contextScope,
279             final ReferenceType referenceType) throws RemoteException, NameNotFoundException, InvalidNameException,
280             NotContextException, NamingException, NullPointerException
281     {
282         return addListener(listener, absolutePath, contextScope, LocalEventProducer.FIRST_POSITION, referenceType);
283     }
284 
285     /**
286      * Add a listener for the provided scope as strong reference to the specified position of a queue of listeners.
287      * @param listener EventListener; the listener which is interested at events of eventType.
288      * @param absolutePath String; the absolute path of the context or object to subscribe to
289      * @param contextScope ContextScope; the part of the tree that the listener is aimed at (current node, current node and
290      *            keys, subtree).
291      * @param position int; the position of the listener in the queue.
292      * @return the success of adding the listener. If a listener was already added, or an illegal position is provided false is
293      *         returned.
294      * @throws NameNotFoundException when the absolutePath could not be found in the parent context, or when an intermediate
295      *             context does not exist
296      * @throws InvalidNameException when the scope is OBJECT_SCOPE, but the key points to a (sub)context
297      * @throws NotContextException when the scope is LEVEL_SCOPE, OBJECT_LEVEL_SCOPE or SUBTREE_SCOPE, and the key points to an
298      *             ordinary object
299      * @throws NamingException as an overarching exception for context errors
300      * @throws NullPointerException when one of the arguments is null
301      * @throws RemoteException if a network connection failure occurs.
302      */
303     public boolean addListener(final EventListener listener, final String absolutePath, final ContextScope contextScope,
304             final int position) throws RemoteException, NameNotFoundException, InvalidNameException, NotContextException,
305             NamingException, NullPointerException
306     {
307         return addListener(listener, absolutePath, contextScope, position, ReferenceType.STRONG);
308     }
309 
310     /**
311      * Add a listener for the provided scope to the specified position of a queue of listeners.
312      * @param listener EventListener; which is interested at certain events,
313      * @param absolutePath String; the absolute path of the context or object to subscribe to
314      * @param contextScope ContextScope; the part of the tree that the listener is aimed at (current node, current node and
315      *            keys, subtree).
316      * @param position int; the position of the listener in the queue
317      * @param referenceType ReferenceType; whether the listener is added as a strong or as a weak reference.
318      * @return the success of adding the listener. If a listener was already added or an illegal position is provided false is
319      *         returned.
320      * @throws InvalidNameException when the path does not start with a slash
321      * @throws NullPointerException when one of the arguments is null
322      * @throws RemoteException if a network connection failure occurs
323      */
324     public boolean addListener(final EventListener listener, final String absolutePath, final ContextScope contextScope,
325             final int position, final ReferenceType referenceType)
326             throws RemoteException, InvalidNameException, NullPointerException
327     {
328         Throw.when(listener == null, NullPointerException.class, "listener cannot be null");
329         Throw.when(absolutePath == null, NullPointerException.class, "absolutePath cannot be null");
330         Throw.when(contextScope == null, NullPointerException.class, "contextScope cannot be null");
331         Throw.when(referenceType == null, NullPointerException.class, "referenceType cannot be null");
332         Throw.when(!absolutePath.startsWith("/"), InvalidNameException.class, "absolute path %s does not start with '/'",
333                 absolutePath);
334 
335         String registryKey = makeRegistryKey(absolutePath, contextScope);
336         boolean added = !this.registryMap.containsKey(registryKey);
337         String regex = makeRegex(absolutePath, contextScope);
338         PatternListener patternListener = new PatternListener(Pattern.compile(regex), listener);
339         this.regExpListenerMap.put(registryKey, patternListener);
340         this.registryMap.put(registryKey, patternListener);
341         return added;
342     }
343 
344     /**
345      * Remove the subscription of a listener for the provided scope for a specific event.
346      * @param listener EventListener; which is no longer interested.
347      * @param absolutePath String; the absolute path of the context or object to subscribe to
348      * @param contextScope ContextScope;the scope which is of no interest any more.
349      * @return the success of removing the listener. If a listener was not subscribed false is returned.
350      * @throws InvalidNameException when the path does not start with a slash
351      * @throws NullPointerException when one of the arguments is null
352      * @throws RemoteException if a network connection failure occurs
353      */
354     public boolean removeListener(final EventListener listener, final String absolutePath, final ContextScope contextScope)
355             throws RemoteException, InvalidNameException, NullPointerException
356     {
357         Throw.when(listener == null, NullPointerException.class, "listener cannot be null");
358         Throw.when(absolutePath == null, NullPointerException.class, "absolutePath cannot be null");
359         Throw.when(contextScope == null, NullPointerException.class, "contextScope cannot be null");
360         Throw.when(!absolutePath.startsWith("/"), InvalidNameException.class, "absolute path %s does not start with '/'",
361                 absolutePath);
362 
363         String registryKey = makeRegistryKey(absolutePath, contextScope);
364         boolean removed = this.registryMap.containsKey(registryKey);
365         this.regExpListenerMap.remove(registryKey);
366         this.registryMap.remove(registryKey);
367         return removed;
368     }
369 
370     /**
371      * Pair of regular expression pattern and event listener.
372      * <p>
373      * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
374      * See for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>.
375      * The DSOL project is distributed under a three-clause BSD-style license, which can be found at
376      * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
377      * https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
378      * </p>
379      * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
380      */
381     public class PatternListener implements Serializable
382     {
383         /** */
384         private static final long serialVersionUID = 20200210L;
385 
386         /** the compiled regular expression pattern. */
387         private final Pattern pattern;
388 
389         /** the event listener for this pattern. */
390         private final EventListener listener;
391 
392         /**
393          * Construct a pattern - listener pair.
394          * @param pattern Pattern; the compiled pattern
395          * @param listener EventListener; the registered listener for this pattern
396          */
397         public PatternListener(final Pattern pattern, final EventListener listener)
398         {
399             super();
400             this.pattern = pattern;
401             this.listener = listener;
402         }
403 
404         /**
405          * return the compiled pattern.
406          * @return Pattern; the compiled pattern
407          */
408         public Pattern getPattern()
409         {
410             return this.pattern;
411         }
412 
413         /**
414          * Return the registered listener for this pattern.
415          * @return EventListener; the registered listener for this pattern
416          */
417         public EventListener getListener()
418         {
419             return this.listener;
420         }
421     }
422 }