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-2023 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-2023 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 }