View Javadoc
1   package nl.tudelft.simulation.naming.context;
2   
3   import java.util.Collection;
4   import java.util.Collections;
5   import java.util.LinkedHashSet;
6   import java.util.Map;
7   import java.util.Set;
8   import java.util.TreeMap;
9   
10  import javax.naming.NameAlreadyBoundException;
11  import javax.naming.NameNotFoundException;
12  import javax.naming.NamingException;
13  import javax.naming.NotContextException;
14  
15  import org.djutils.event.LocalEventProducer;
16  import org.djutils.exceptions.Throw;
17  
18  import nl.tudelft.simulation.naming.context.util.ContextUtil;
19  
20  /**
21   * The JvmContext is an in-memory, thread-safe context implementation of the ContextInterface.
22   * <p>
23   * Copyright (c) 2020-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
24   * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
25   * project is distributed under a three-clause BSD-style license, which can be found at
26   * <a href="https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
27   * </p>
28   * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
29   */
30  public class JvmContext extends LocalEventProducer implements ContextInterface
31  {
32      /** the parent context. */
33      protected ContextInterface parent;
34  
35      /** the atomicName. */
36      private String atomicName;
37  
38      /** the absolute path of this context. */
39      private String absolutePath;
40  
41      /** the children. */
42      protected Map<String, Object> elements = Collections.synchronizedMap(new TreeMap<String, Object>());
43  
44      /**
45       * constructs a new root JvmContext.
46       * @param atomicName the name under which the root context will be registered
47       */
48      public JvmContext(final String atomicName)
49      {
50          this(null, atomicName);
51      }
52  
53      /**
54       * Constructs a new JvmContext.
55       * @param parent the parent context
56       * @param atomicName the name under which the context will be registered
57       */
58      public JvmContext(final ContextInterface parent, final String atomicName)
59      {
60          Throw.whenNull(atomicName, "name under which a context is registered cannot be null");
61          Throw.when(atomicName.contains(ContextInterface.SEPARATOR), IllegalArgumentException.class,
62                  "name %s under which a context is registered cannot contain the separator string %s", atomicName,
63                  ContextInterface.SEPARATOR);
64          this.parent = parent;
65          this.atomicName = atomicName;
66          this.absolutePath = parent == null ? "" : parent.getAbsolutePath() + ContextInterface.SEPARATOR + this.atomicName;
67      }
68  
69      @Override
70      public String getAtomicName()
71      {
72          return this.atomicName;
73      }
74  
75      @Override
76      public ContextInterface getParent()
77      {
78          return this.parent;
79      }
80  
81      @Override
82      public ContextInterface getRootContext()
83      {
84          ContextInterface result = this;
85          while (result.getParent() != null)
86          {
87              result = result.getParent();
88          }
89          return result;
90      }
91  
92      @Override
93      public String getAbsolutePath()
94      {
95          return this.absolutePath;
96      }
97  
98      @Override
99      public Object getObject(final String key) throws NamingException
100     {
101         Throw.whenNull(key, "key cannot be null");
102         Throw.when(key.length() == 0 || key.contains(ContextInterface.SEPARATOR), NamingException.class,
103                 "key [%s] is the empty string or key contains '/'", key);
104         if (!this.elements.containsKey(key))
105         {
106             throw new NameNotFoundException("key " + key + " does not exist in Context");
107         }
108         // can be null -- null objects are allowed in the context tree
109         return this.elements.get(key);
110     }
111 
112     @Override
113     public Object get(final String name) throws NamingException
114     {
115         ContextName contextName = lookup(name);
116         if (contextName.getName().length() == 0)
117         {
118             return contextName.getContext();
119         }
120         Object result = contextName.getContext().getObject(contextName.getName());
121         return result;
122     }
123 
124     @Override
125     public boolean exists(final String name) throws NamingException
126     {
127         ContextName contextName = lookup(name);
128         if (contextName.getName().length() == 0)
129         {
130             return true;
131         }
132         return contextName.getContext().hasKey(contextName.getName());
133     }
134 
135     @Override
136     public boolean hasKey(final String key) throws NamingException
137     {
138         Throw.whenNull(key, "key cannot be null");
139         Throw.when(key.length() == 0 || key.contains(ContextInterface.SEPARATOR), NamingException.class,
140                 "key [%s] is the empty string or key contains '/'", key);
141         return this.elements.containsKey(key);
142     }
143 
144     /**
145      * Indicates whether the object has been registered (once or more) in the current Context. The object may be null.
146      * @param object the object to look up; mey be null
147      * @return whether an object with the given key has been registered once or more in the current context
148      */
149     @Override
150     public boolean hasObject(final Object object)
151     {
152         return this.elements.containsValue(object);
153     }
154 
155     @Override
156     public boolean isEmpty()
157     {
158         return this.elements.isEmpty();
159     }
160 
161     @Override
162     public void bindObject(final String key, final Object object) throws NamingException
163     {
164         Throw.whenNull(key, "key cannot be null");
165         Throw.when(key.length() == 0 || key.contains(ContextInterface.SEPARATOR), NamingException.class,
166                 "key [%s] is the empty string or key contains '/'", key);
167         if (this.elements.containsKey(key))
168         {
169             throw new NameAlreadyBoundException("key " + key + " already bound to object in Context");
170         }
171         checkCircular(object);
172         this.elements.put(key, object);
173         fireEvent(ContextInterface.OBJECT_ADDED_EVENT, new Object[] {getAbsolutePath(), key, object});
174     }
175 
176     @Override
177     public void bindObject(final Object object) throws NamingException
178     {
179         Throw.whenNull(object, "object cannot be null");
180         bindObject(makeObjectKey(object), object);
181     }
182 
183     @Override
184     public void bind(final String name, final Object object) throws NamingException
185     {
186         ContextName contextName = lookup(name);
187         contextName.getContext().bindObject(contextName.getName(), object);
188     }
189 
190     @Override
191     public void unbindObject(final String key) throws NamingException
192     {
193         Throw.whenNull(key, "key cannot be null");
194         Throw.when(key.length() == 0 || key.contains(ContextInterface.SEPARATOR), NamingException.class,
195                 "key [%s] is the empty string or key contains '/'", key);
196         if (this.elements.containsKey(key))
197         {
198             Object object = this.elements.remove(key);
199             fireEvent(ContextInterface.OBJECT_REMOVED_EVENT, new Object[] {getAbsolutePath(), key, object});
200         }
201     }
202 
203     @Override
204     public void unbind(final String name) throws NamingException
205     {
206         ContextName contextName = lookup(name);
207         contextName.getContext().unbindObject(contextName.getName());
208     }
209 
210     @Override
211     public void rebindObject(final String key, final Object object) throws NamingException
212     {
213         Throw.whenNull(key, "key cannot be null");
214         Throw.when(key.length() == 0 || key.contains(ContextInterface.SEPARATOR), NamingException.class,
215                 "key [%s] is the empty string or key contains '/'", key);
216         checkCircular(object);
217         if (this.elements.containsKey(key))
218         {
219             this.elements.remove(key);
220             fireEvent(ContextInterface.OBJECT_REMOVED_EVENT, new Object[] {getAbsolutePath(), key, object});
221         }
222         this.elements.put(key, object);
223         fireEvent(ContextInterface.OBJECT_ADDED_EVENT, new Object[] {getAbsolutePath(), key, object});
224     }
225 
226     @Override
227     public void rebind(final String name, final Object object) throws NamingException
228     {
229         ContextName contextName = lookup(name);
230         contextName.getContext().rebindObject(contextName.getName(), object);
231     }
232 
233     @Override
234     public void rename(final String oldName, final String newName) throws NamingException
235     {
236         ContextName contextNameOld = lookup(oldName);
237         ContextName contextNameNew = lookup(newName);
238         if (contextNameNew.getContext().hasKey(contextNameNew.getName()))
239         {
240             throw new NameAlreadyBoundException("key " + newName + " already bound to object in Context");
241         }
242         Object object = contextNameOld.getContext().getObject(contextNameOld.getName());
243         contextNameNew.getContext().checkCircular(object);
244         contextNameOld.getContext().unbindObject(contextNameOld.getName());
245         contextNameNew.getContext().bindObject(contextNameNew.getName(), object);
246     }
247 
248     @Override
249     public ContextInterface createSubcontext(final String name) throws NamingException
250     {
251         return lookupAndBuild(name);
252     }
253 
254     @Override
255     public void destroySubcontext(final String name) throws NamingException
256     {
257         ContextName contextName = lookup(name);
258         Object object = contextName.getContext().getObject(contextName.getName());
259         if (!(object instanceof ContextInterface))
260         {
261             throw new NotContextException("name " + name + " is bound but does not name a context");
262         }
263         destroy((ContextInterface) object);
264         contextName.getContext().unbindObject(contextName.getName());
265     }
266 
267     /**
268      * Take a (compound) name such as "sub1/sub2/key" or "key" or "/sub/key" and lookup and/or create all intermediate contexts
269      * as well as the final sub-context of the path.
270      * @param name the (possibly compound) name
271      * @return the context, possibly built new
272      * @throws NamingException as a placeholder overarching exception
273      * @throws NameNotFoundException when an intermediate context does not exist
274      * @throws NullPointerException when name is null
275      */
276     protected ContextInterface lookupAndBuild(final String name) throws NamingException
277     {
278         Throw.whenNull(name, "name cannot be null");
279 
280         // Handle current context lookup
281         if (name.length() == 0 || name.equals(ContextInterface.SEPARATOR))
282         {
283             throw new NamingException("the terminal reference is '/' or empty");
284         }
285 
286         // determine absolute or relative path
287         String reference;
288         ContextInterface subContext;
289         if (name.startsWith(ContextInterface.ROOT))
290         {
291             reference = name.substring(ContextInterface.SEPARATOR.length());
292             subContext = getRootContext();
293         }
294         else
295         {
296             reference = name;
297             subContext = this;
298         }
299 
300         while (true)
301         {
302             int index = reference.indexOf(ContextInterface.SEPARATOR);
303             if (index == -1)
304             {
305                 ContextInterface newContext = new JvmContext(subContext, reference);
306                 subContext.bind(reference, newContext);
307                 subContext = newContext;
308                 break;
309             }
310             String sub = reference.substring(0, index);
311             reference = reference.substring(index + ContextInterface.SEPARATOR.length());
312             if (!subContext.hasKey(sub))
313             {
314                 ContextInterface newContext = new JvmContext(subContext, sub);
315                 subContext.bind(sub, newContext);
316                 subContext = newContext;
317             }
318             else
319             {
320                 Object subObject = subContext.getObject(sub); // can throw NameNotFoundException
321                 if (!(subObject instanceof ContextInterface))
322                 {
323                     throw new NameNotFoundException(
324                             "parsing name " + name + " in context -- bound object " + sub + " is not a subcontext");
325                 }
326                 subContext = (ContextInterface) subObject;
327             }
328         }
329         return subContext;
330     }
331 
332     /**
333      * Recursively unbind and destroy all keys and subcontexts from the given context, leaving it empty. All removals will fire
334      * an OBJECT_REMOVED event, depth first.
335      * @param context the context to empty
336      * @throws NamingException on tree inconsistencies
337      */
338     protected void destroy(final ContextInterface context) throws NamingException
339     {
340         // depth-first subcontext removal
341         Set<String> copyKeySet = new LinkedHashSet<>(context.keySet());
342         for (String key : copyKeySet)
343         {
344             if (context.getObject(key) instanceof ContextInterface && context.getObject(key) != null)
345             {
346                 destroy((ContextInterface) context.getObject(key));
347                 context.unbindObject(key);
348             }
349         }
350 
351         // leaf removal
352         copyKeySet = new LinkedHashSet<>(context.keySet());
353         for (String key : copyKeySet)
354         {
355             if (context.getObject(key) instanceof ContextInterface)
356             {
357                 throw new NamingException("Tree inconsistent -- Context not removed or added during destroy operation");
358             }
359             context.unbindObject(key);
360         }
361     }
362 
363     @Override
364     public Set<String> keySet()
365     {
366         return this.elements.keySet();
367     }
368 
369     @Override
370     public Collection<Object> values()
371     {
372         return this.elements.values();
373     }
374 
375     @Override
376     public Map<String, Object> bindings()
377     {
378         return this.elements;
379     }
380 
381     @Override
382     public void fireObjectChangedEventValue(final Object object)
383             throws NameNotFoundException, NullPointerException, NamingException
384     {
385         Throw.whenNull(object, "object cannot be null");
386         fireObjectChangedEventKey(makeObjectKey(object));
387     }
388 
389     @Override
390     public void fireObjectChangedEventKey(final String key) throws NameNotFoundException, NullPointerException, NamingException
391     {
392         Throw.whenNull(key, "key cannot be null");
393         Throw.when(key.length() == 0 || key.contains(ContextInterface.SEPARATOR), NamingException.class,
394                 "key [%s] is the empty string or key contains '/'", key);
395         if (!hasKey(key))
396         {
397             throw new NameNotFoundException("Could not find object with key " + key + " for fireObjectChangedEvent");
398         }
399         try
400         {
401             fireEvent(ContextInterface.OBJECT_CHANGED_EVENT, new Object[] {getAbsolutePath(), key, getObject(key)});
402         }
403         catch (Exception exception)
404         {
405             throw new NamingException(exception.getMessage());
406         }
407     }
408 
409     /**
410      * Make a key for the object based on object.toString() where the "/" characters are replaced by "#".
411      * @param object the object for which the key has to be generated
412      * @return the key based on toString() where the "/" characters are replaced by "#"
413      */
414     private String makeObjectKey(final Object object)
415     {
416         return object.toString().replace(ContextInterface.SEPARATOR, ContextInterface.REPLACE_SEPARATOR);
417     }
418 
419     @Override
420     public void checkCircular(final Object newObject) throws NamingException
421     {
422         if (!(newObject instanceof ContextInterface))
423             return;
424         for (ContextInterface parentContext = getParent(); parentContext != null; parentContext = parentContext.getParent())
425         {
426             if (parentContext.equals(newObject))
427             {
428                 throw new NamingException(
429                         "circular reference for object " + newObject + "; prevented insertion into " + toString());
430             }
431         }
432     }
433 
434     @Override
435     public void close() throws NamingException
436     {
437         this.elements.clear();
438         this.atomicName = "";
439         this.parent = null;
440     }
441 
442     @Override
443     public String toString()
444     {
445         String parentName;
446         parentName = this.parent == null ? "null" : this.parent.getAtomicName();
447         return "JvmContext[parent=" + parentName + ", atomicName=" + this.atomicName + "]";
448     }
449 
450     @Override
451     public String toString(final boolean verbose)
452     {
453         if (!verbose)
454         {
455             return "JvmContext[" + getAtomicName() + "]";
456         }
457         return ContextUtil.toText(this);
458     }
459 
460     /**
461      * Take a (compound) name such as "sub1/sub2/key" or "key" or "/sub/key" and lookup the final sub-context of the path, and
462      * store it in the ContextName. Store the key String in the ContextName without checking whether it exists.
463      * @param name the (possibly compound) name
464      * @return a ContextName combination string the subcontext and final reference name
465      * @throws NamingException as a placeholder overarching exception
466      * @throws NameNotFoundException when an intermediate context does not exist
467      * @throws NullPointerException when name is null
468      */
469     protected ContextName lookup(final String name) throws NamingException
470     {
471         Throw.whenNull(name, "name cannot be null");
472 
473         // Handle current context lookup
474         if (name.length() == 0)
475         {
476             return new ContextName(this, "");
477         }
478 
479         // determine absolute or relative path
480         String reference;
481         ContextInterface subContext;
482         if (name.startsWith(ContextInterface.ROOT))
483         {
484             reference = name.substring(ContextInterface.SEPARATOR.length());
485             subContext = getRootContext();
486         }
487         else
488         {
489             reference = name;
490             subContext = this;
491         }
492 
493         while (true)
494         {
495             int index = reference.indexOf(ContextInterface.SEPARATOR);
496             if (index == -1)
497             {
498                 break;
499             }
500             String sub = reference.substring(0, index);
501             reference = reference.substring(index + ContextInterface.SEPARATOR.length());
502             Object subObject = subContext.getObject(sub); // can throw NameNotFoundException
503             if (!(subObject instanceof ContextInterface))
504             {
505                 throw new NameNotFoundException(
506                         "parsing name " + name + " in context -- bound object " + sub + " is not a subcontext");
507             }
508             subContext = (ContextInterface) subObject;
509         }
510         return new ContextName(subContext, reference);
511     }
512 
513     /**
514      * Record with Context and Name combination.
515      * <p>
516      * Copyright (c) 2020-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
517      * See for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The
518      * DSOL project is distributed under a three-clause BSD-style license, which can be found at
519      * <a href="https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
520      * https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
521      * </p>
522      * @author <a href="https://github.com/averbraeck" target="_blank">Alexander Verbraeck</a>
523      */
524     public static class ContextName
525     {
526         /** the context. */
527         private final ContextInterface context;
528 
529         /** the name (key) in the context. */
530         private final String name;
531 
532         /**
533          * Instantiate a Context and Name combination.
534          * @param context the context
535          * @param name the name (key) in the context
536          */
537         public ContextName(final ContextInterface context, final String name)
538         {
539             this.context = context;
540             this.name = name;
541         }
542 
543         /**
544          * @return context
545          */
546         public ContextInterface getContext()
547         {
548             return this.context;
549         }
550 
551         /**
552          * @return name
553          */
554         public String getName()
555         {
556             return this.name;
557         }
558 
559         @Override
560         public int hashCode()
561         {
562             final int prime = 31;
563             int result = 1;
564             result = prime * result + ((this.context == null) ? 0 : this.context.hashCode());
565             result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
566             return result;
567         }
568 
569         @Override
570         public boolean equals(final Object obj)
571         {
572             if (this == obj)
573                 return true;
574             if (obj == null)
575                 return false;
576             if (getClass() != obj.getClass())
577                 return false;
578             ContextName other = (ContextName) obj;
579             if (this.context == null)
580             {
581                 if (other.context != null)
582                     return false;
583             }
584             else if (!this.context.equals(other.context))
585                 return false;
586             if (this.name == null)
587             {
588                 if (other.name != null)
589                     return false;
590             }
591             else if (!this.name.equals(other.name))
592                 return false;
593             return true;
594         }
595 
596         @Override
597         public String toString()
598         {
599             return "ContextName[context=" + this.context + ", name=" + this.name + "]";
600         }
601     }
602 
603 }