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