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 }