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