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 }